Панель отправка исходящих факсов средствами Asterisk и Node.js

в 16:55, , рубрики: Без рубрики

Факс — это одна из тех вещей, которой многие желают скорейшей смерти. Тем не менее в регионах этот способ передачи информации по прежнему используется очень часто. Так и в нашей организации появилась необходимость по возможности упростить данный процесс. После изучения уже существующих здесь статей я пришел к выводу, что представленные решения не совсем подходят в моей ситуации. В частности, хотелось немного более интеллектуальную систему, чем просто основанную на call файлах. Такую, чтобы она могла перезванивать несколько раз в случае неудачной отправке. При этом пользователь должен видеть текущее состояние доставки. В совокупности с тем, что мне давно хотелось посмотреть на веб-разработку в целом и node.js в частности, было принято решение написать свой велосипед сервер исходящих факсов. Что из этого получилось можно увидеть под катом.

В первую очередь необходимо было настроить Asterisk так, чтобы он создавал события, отображающие продвижение факса в реально времени. Ниже приведен фрагмент плана набора Asterisk, который позволит нам делать это, а так же обрабатывать следующие ситуации:

  • Не удалось дозвониться до абонента (например занята линия или трубка не была поднята до истечения таймаута).
  • Абонент снял трубку и положил ее до того, как началась передача факса (т.е. в момент воспроизведения приветствия).
  • Передача факса началась успешно, но не завершилась.

Кроме этого план набора позволяет ограничить число одновременных исходящих факс-вызовов, чтобы случайно не занять все свободных SIP каналы.

[OutgoingFaxInit]
; Этот экстеншен используется для совершения исходящего звонка 
exten => _X.,1,NoOp()
 same => n,Set(GROUP()=faxout)
; Запоминаем количество одновременных факс-соединений в БД Asterisk,
; чтобы failed экстеншен мог отличать превышение одновременных от ситуации,
; когда соединение просто не удалось.
 same => n,Set(DB(fax_group_count/${UUID})=${GROUP_COUNT(faxout)})
 same => n,GotoIf($[${DB(fax_group_count/${UUID})}<=${MAX_PARALLELISM}]?call)
 same => n,UserEvent(Fax,uuid: ${UUID},Status: CALL SUSPENDED)
 same => n,HangUp()
 same => n(call),Dial(Local/${EXTEN}@OutgoingCalls)
 same => n,HangUp()

; Этот экстеншен будет выступать источником данных с нашей стороны
exten => router,1,NoOp()
 same => n,Set(__UUID=${UUID})
 same => n,Set(__DATA=${DATA})
 same => n,Dial(Local/fax@OutgoingFax)
 same => n,HangUp()

exten => failed,1,NoOp()
; В случае, если факс был прерван из-за превышения числа соединений UserEvent
; создаваться не будет
 same => n,GotoIf($[${DB_DELETE(fax_group_count/${UUID})}<=${MAX_PARALLELISM}]?:end)
 same => n,UserEvent(Fax,uuid: ${UUID},Status: CALL PICKUP FAILED)
 same => n(end),HangUp()

[OutgoingFax]
exten => fax,1,NoOp()
 same => n,UserEvent(Fax,uuid: ${UUID},Status: CALL PICKUP SUCCESS);
; Передача факса еще не началась. Запоминаем это.
 same => n,Set(DB(fax_sendstatus/${UUID})=0)
 same => n,Playback(autofax)
 same => n,Set(FAXOPT(headerinfo)=Company)
 same => n,Set(FAXOPT(localstationid)=XXX-XX-XX)
; Началась передача факса
 same => n,Set(DB(fax_sendstatus/${UUID})=1)
 same => n,SendFax(${DATA})
 same => n,HangUp()

exten => h,1,NoOp()
; Если передача факса не была начата, то генерируем событие
; неудачной отправки
 same => n,GotoIf($[${DB_DELETE(fax_sendstatus/${UUID})}]?sendstatus)
 same => n,UserEvent(Fax,uuid: ${UUID},Status: FAX SEND FAILED)
 same => n,Goto(end)
; Если мы начали передавать факс, то сообщаем данные из ${FAXOPT}
 same => n(sendstatus),UserEvent(Fax,uuid: ${UUID},Status: FAX SEND ${FAXOPT(status)})
 same => n(end),NoOp()

Основная часть нашего факс-сервера, как было отмечено выше, будет работать на node.js. С Asterisk мы будем взаимодействовать по AMI. Для полноценной работы клиенту будет достаточно прав на создание вызовов и чтения UserEvent'ов. Таким образом manager.conf будет иметь следующий вид:

[general]
enabled=yes

[FAX]
secret=password
read=user
write=originate

Для работы с AMI был выбран модуль nami. В отличие аналог он подкупает достаточно большим функционалом из коробки. Есть уже готовые методы для работы с большей частью событий и генерации Action'ов. Стоит отметить, что у автора данного модуля есть реализации AMI интерфейсов и для других языков, кроме JS.

Общий механизм работы факс-сервера следующий:

  • Пользователь заходит на специальную веб страницу и в окне создания нового факса указывает файл для отправки (PDF) и номер получателя.
  • Загруженный файл помещается на сервер и конвертируется в поддерживаемый Asterisk .tiff формат. Факсу присваивается UUID.
  • Вся информация о факсе (время отправки, номер получателя, UUID, количестве попыток повтора) сохраняется в базе данных.
  • При появлении нового факса в очереди выполняется его отправка и перемещение в очередь обрабатываемых факсов
  • Если через AMI будет получено событие об ошибке отправки, то количество попыток отправки для данного факса будет увеличено, а сам факс будет перемещено в очередь отложенных.
  • По истечении попыток отправки факс помечается как недоставленный.

Для реализации очередь и самой базы данных используется Redis. Структура хранения данных следующая:

  • Key-value для каждой характеристики факса. Ключи имеют вид fax:uuid:field. Первоначально предполагалось хранить все данные по одному ключу в виде JSON, но потом я решил, что каждый раз парсить и снова сериализовать JSON для изменения какой-либо информации, будет достаточно глупо.
  • Сама очередь факсов хранится в виде LIST с ключом fax:send. Подписка на новые факсы в очередь реализована через команду BLPOP.
  • Все обрабатываемые факсы хранятся в Sorted Set с ключом fax:processing. В качестве веса выступает время поступления в очередь.
  • Для реализации задачи перезвонов через заданный интервал используется еще один Sorted Set fax:delayed, так же с временным весом
  • Успешные и недоставленные факсы сохраняются в fax:failed и fax:success.

Для создания приятной глазу веб-формы был выбран twitter bootstrap. Отображение информации пользователю сделано через jQuery datatables. Тут меня поджидала проблема с тем, что jQuery datatables не адаптирован к текущий версии bootstrap 3. К счастью на github был репозиторий исправленной версии.
В конечном итоге получилось следующее:
image
image
Все основные настройки расположены в config.json:

{
    "logLevel": "info",
    "port": 80,  // порт веб сервера
    "FAX": {
        "uploadDir": "/tmp/faxout",  // каталог для загрузок
        "storageDir": "/tmp/faxout", // каталог для хранения TIFF файлов факсов
        "gsCommand": "gs", // команда вызова Ghostscript (им конвертируются входящие файлы)
        "maxParallelism": 3, // максимально количество одновременных вызовов
        "maxRetry": 5, // максимально количество повторных факсов
        "retryInterval": 420, // интервал повтора факсов
        "delayedProcessingInterval": 5 // интервал проверки очереди отложенных факсов
    },
    "AMI": { // данные для подключения к AMI
        "host": "192.168.1.1",
        "port": 5038,
        "username": "FAX",
        "secret": "password"
    }

Получить исходный код можно на github. Для работы необходимо добавить в план набора Asterisk описанный выше фрагмент а так же иметь на сервере redis и node.js. Надеюсь мой «Hello world» (он же факс-сервер) на node.js окажется вам полезным.

Автор: Infactum

Источник

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


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