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