├── .bowerrc ├── .gitignore ├── README.md ├── app.js ├── bower.json ├── config ├── config.json └── index.js ├── lib ├── faxAMI.js ├── faxDB.js ├── faxProducer.js └── socketioLogWrapper.js ├── package.json ├── public ├── favicon.ico └── js │ ├── main.js │ └── modal.js ├── routes ├── index.js ├── state.js ├── translate.js └── upload.js ├── translate ├── en.json └── ru.json └── views ├── head.jade ├── index.jade └── layout.jade /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory" : "public/lib" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/lib/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | faxserver 2 | ========= 3 | FaxServer is ExpressJS web-GUI for smart handling outgoing t.38 faxes through Asterisk. More info can be found [here](http://habrahabr.ru/post/207080/). 4 | Requirements 5 | ------------ 6 | 1. Asterisk with AMI and fax dialplan (see below) 7 | 2. Node.JS 8 | 3. Redis server 9 | 10 | Installation 11 | ------------ 12 | 1. Clone this repo 13 | 2. npm install 14 | 3. bower install 15 | 4. node app.js 16 | 17 | Asterisk configuration 18 | ---------------------- 19 | You should add below code to the asterisk dialplan. This will make Asterisk generated user events with fax status information. 20 | 21 | ``` 22 | [OutgoingFaxInit] 23 | exten => _X.,1,NoOp() 24 | same => n,Set(GROUP()=faxout) 25 | same => n,Set(DB(fax_group_count/${UUID})=${GROUP_COUNT(faxout)}) 26 | same => n,GotoIf($[${DB(fax_group_count/${UUID})}<=${MAX_PARALLELISM}]?call) 27 | same => n,UserEvent(Fax,uuid: ${UUID},Status: CALL SUSPENDED) 28 | same => n,HangUp() 29 | same => n(call),Dial(Local/${EXTEN}@OutgoingCalls) 30 | same => n,HangUp() 31 | 32 | exten => router,1,NoOp() 33 | same => n,Set(__UUID=${UUID}) 34 | same => n,Set(__DATA=${DATA}) 35 | same => n,Dial(Local/fax@OutgoingFax) 36 | same => n,HangUp() 37 | 38 | exten => failed,1,NoOp() 39 | same => n,GotoIf($[${DB_DELETE(fax_group_count/${UUID})}<=${MAX_PARALLELISM}]?:end) 40 | same => n,UserEvent(Fax,uuid: ${UUID},Status: CALL PICKUP FAILED) 41 | same => n(end),HangUp() 42 | 43 | [OutgoingFax] 44 | exten => fax,1,NoOp() 45 | same => n,UserEvent(Fax,uuid: ${UUID},Status: CALL PICKUP SUCCESS); 46 | same => n,Set(DB(fax_sendstatus/${UUID})=0) 47 | same => n,Playback(autofax) 48 | same => n,Set(FAXOPT(headerinfo)=Company) 49 | same => n,Set(FAXOPT(localstationid)=XXX-XX-XX) 50 | same => n,Set(DB(fax_sendstatus/${UUID})=1) 51 | same => n,SendFax(${DATA}) 52 | same => n,HangUp() 53 | 54 | exten => h,1,NoOp() 55 | same => n,GotoIf($[${DB_DELETE(fax_sendstatus/${UUID})}]?sendstatus) 56 | same => n,UserEvent(Fax,uuid: ${UUID},Status: FAX SEND FAILED) 57 | same => n,Goto(end) 58 | same => n(sendstatus),UserEvent(Fax,uuid: ${UUID},Status: FAX SEND ${FAXOPT(status)}) 59 | same => n(end),NoOp() 60 | ``` 61 | You should also enable Asterisk AMI and add user with propper rights like this: 62 | ``` 63 | [general] 64 | enabled=yes 65 | 66 | [FAX] 67 | secret=password 68 | read=user 69 | write=originate 70 | ``` 71 | 72 | Configuration 73 | ------------- 74 | All settings are avaliable in config/config.json. Here is some hints on them: 75 | __port__ - Port of webserver. 76 | __language__ - Overall transltation. EN or RU avaliable. Other languages can be added in translation folder. 77 | __uploadDir__ - Distanation where PDF files will be uploaded. 78 | __storageDir__ - Distanation where TIFF faxes will be stored. 79 | __gsCommand__ - Command for GhostScript (used for PDF->TIFF converion). 80 | __maxParallelism__ - Number of maximum simultaneous fax calls. 81 | __maxRetry__ - How much times try to send fax before fail. 82 | __retryInterval__ - Interval beetween retry calls in seconds. 83 | __delayedProcessingInterval__ - Interval beetween delayed faxes queue check in seconds. 84 | __AMI__ - Settings of Asterisk AMI connection. 85 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var config = require('./config'), 2 | log4js = require('log4js'), 3 | logger = log4js.getLogger('App'); 4 | 5 | log4js.setGlobalLogLevel(config.get('logLevel')); 6 | 7 | var path = require('path'); 8 | 9 | var http = require('http'), 10 | express = require('express'), 11 | app = express(), 12 | server = http.createServer(app), 13 | io = require('socket.io').listen(server, { 14 | logger: require('./lib/socketioLogWrapper') 15 | }); 16 | 17 | var faxAMI = require('./lib/faxAMI'); 18 | 19 | app.set('port', config.get('port')); 20 | app.set('views', path.join(__dirname, 'views')); 21 | app.set('view engine', 'jade'); 22 | 23 | app.use(express.favicon(path.join(__dirname, 'public/favicon.ico'))); 24 | app.use(app.router); 25 | app.use(express.static(path.join(__dirname, 'public'))); 26 | app.use(log4js.connectLogger(logger, {level: 'auto', format: ':status :method :url' })); 27 | 28 | if ('development' == app.get('env')) { 29 | logger.info('Running in DEV mode'); 30 | app.use(express.errorHandler()); 31 | app.locals.pretty = true; 32 | } 33 | 34 | app.get('/', require('./routes').index); 35 | app.get('/state', require('./routes/state').faxState); 36 | app.get('/translate', require('./routes/translate').getTranslation); 37 | app.post('/upload', require('./routes/upload').upload); 38 | 39 | server.listen(app.get('port'), function () { 40 | logger.info('Express server listening on port ' + app.get('port')); 41 | }); 42 | 43 | require('./lib/faxDB').setEmitter(io.sockets); 44 | 45 | faxAMI.start(); 46 | 47 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FaxServer", 3 | "version": "0.0.1", 4 | "main": "app.js", 5 | "license": "MIT", 6 | "private": true, 7 | "dependencies": { 8 | "bootstrap": "~3.0.3", 9 | "jquery.inputmask": "~2.4.0", 10 | "datatables-plugins": "*", 11 | "datatables": "~1.9.4" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "logLevel": "warn", 3 | "port": 80, 4 | "language": "en", 5 | "FAX": { 6 | "uploadDir": "/tmp/upload", 7 | "storageDir": "/tmp/faxout", 8 | "gsCommand": "gs", 9 | "maxParallelism": 3, 10 | "maxRetry": 3, 11 | "retryInterval": 300, 12 | "delayedProcessingInterval": 5 13 | }, 14 | "AMI": { 15 | "host": "127.0.0.1", 16 | "port": 5038, 17 | "username": "FAX", 18 | "secret": "password" 19 | } 20 | } -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'), 3 | nconf = require('nconf'); 4 | 5 | nconf.use('file', { file: path.join(__dirname, 'config.json') }); 6 | 7 | module.exports = nconf; -------------------------------------------------------------------------------- /lib/faxAMI.js: -------------------------------------------------------------------------------- 1 | var faxDB = require('./faxDB'), 2 | logger = require('log4js').getLogger('faxAMI'), 3 | namiLib = require('nami'), 4 | config = require('../config'), 5 | path = require('path'); 6 | 7 | var nami = new namiLib.Nami(config.get('AMI')); 8 | 9 | nami.logger.setLevel(config.get('logLevel')); 10 | 11 | function start() { 12 | faxDB.getFirstQueuedFaxUDID(onFirstQueuedFaxUDID); 13 | nami.open(); 14 | } 15 | 16 | function onFirstQueuedFaxUDID(err,udid) { 17 | if (err) throw err; 18 | faxDB.getFaxData(udid, onFaxData); 19 | faxDB.getFirstQueuedFaxUDID(onFirstQueuedFaxUDID); 20 | } 21 | 22 | function onFaxData(err,faxData) { 23 | if (err) throw err; 24 | originateCall(faxData); 25 | } 26 | 27 | function originateCall (fax) { 28 | 29 | action = new namiLib.Actions.Originate(); 30 | action.Channel = 'Local/' + fax.phone + '@OutgoingFaxInit'; 31 | 32 | action.Context = 'OutgoingFaxInit'; 33 | action.Exten = 'router'; 34 | action.Async = 'true'; 35 | action.Priority = '1'; 36 | action.variables = { 37 | 'UUID': fax.uuid, 38 | 'MAX_PARALLELISM': config.get('FAX:maxParallelism'), 39 | 'DATA': path.join(config.get('FAX:storageDir'), fax.uuid + '.tiff') 40 | }; 41 | 42 | logger.debug('Originating call %s to number %s (try %s)',fax.uuid,fax.phone,fax.retry+1); 43 | nami.send(action); 44 | } 45 | 46 | nami.on('namiEventUserEvent', function(event) { 47 | if (event.userevent == 'Fax') { 48 | var status = event.status.toUpperCase(); 49 | var uuid = event.uuid; 50 | logger.debug('Fax %s: %s',uuid,status); 51 | switch (status) { 52 | case 'CALL PICKUP SUCCESS': 53 | break; 54 | case 'CALL PICKUP FAILED': 55 | faxDB.incFaxFailure(uuid); 56 | break; 57 | case 'FAX SEND FAILED': 58 | faxDB.incFaxFailure(uuid); 59 | break; 60 | case 'CALL SUSPENDED': 61 | faxDB.suspendFax(uuid); 62 | break; 63 | case 'FAX SEND SUCCESS': 64 | faxDB.setFaxSuccess(uuid); 65 | break; 66 | default: 67 | } 68 | } 69 | }); 70 | 71 | module.exports.start = start; -------------------------------------------------------------------------------- /lib/faxDB.js: -------------------------------------------------------------------------------- 1 | var redis = require('redis'), 2 | redisIO = redis.createClient(), 3 | redisListener = redis.createClient(), 4 | logger = require('log4js').getLogger('FaxDB'), 5 | config = require('../config'), 6 | async = require('async'), 7 | emitter = {}; 8 | 9 | var translation = require('../translate/' + config.get('language')); 10 | 11 | var FaxDB = { 12 | addFaxData: addFaxData, 13 | getFaxData: getFaxData, 14 | getFirstQueuedFaxUDID: getFirstQueuedFaxUDID, 15 | incFaxFailure: incFaxFailure, 16 | suspendFax: suspendFax, 17 | setFaxSuccess: setFaxSuccess, 18 | getCurrentState: getCurrentState, 19 | setEmitter: function() { 20 | emitter = arguments[0]; 21 | } 22 | }; 23 | 24 | function sendUpdateToClients() { 25 | emitter.emit('updateFaxDataTable'); 26 | } 27 | 28 | function addFaxData(uuid, phone) { 29 | var multi = redisIO.multi(); 30 | multi.set('fax:' + uuid + ':phone', phone); 31 | multi.set('fax:' + uuid + ':date', new Date().getTime()); 32 | multi.set('fax:' + uuid + ':retry', 0); 33 | multi.rpush('fax:send', uuid); 34 | multi.exec(function(err) { 35 | if (err) throw err; 36 | logger.debug('fax %s for %s added to DB', uuid, phone); 37 | sendUpdateToClients(); 38 | }); 39 | } 40 | 41 | function getFaxData(uuid, callback) { 42 | var multi = redisIO.multi(); 43 | multi.get('fax:' + uuid + ':phone'); 44 | multi.get('fax:' + uuid + ':retry'); 45 | multi.get('fax:' + uuid + ':date'); 46 | multi.exec(function(err,replies) { 47 | var fax = { 48 | uuid: uuid, 49 | phone: replies[0], 50 | retry: parseInt(replies[1]), 51 | date: new Date(parseInt(replies[2])) 52 | }; 53 | callback(err,fax); 54 | }); 55 | } 56 | 57 | function getFirstQueuedFaxUDID(callback) { 58 | redisListener.blpop('fax:send',0,function(err, result) { 59 | if (err) { 60 | callback(err); 61 | } else { 62 | redisIO.zadd('fax:processing',new Date().getTime(),result[1]); 63 | callback(err, result[1]); 64 | } 65 | }); 66 | } 67 | 68 | function incFaxFailure(uuid) { 69 | redisIO.incr('fax:' + uuid + ':retry', function(err,result) { 70 | if (err) throw err; 71 | if (parseInt(result) < config.get('FAX:maxRetry')) { 72 | suspendFax(uuid); 73 | } else { 74 | setFaxFailure(uuid); 75 | } 76 | sendUpdateToClients(); 77 | }); 78 | } 79 | 80 | function suspendFax(uuid) { 81 | var multi = redisIO.multi(); 82 | multi.zrem('fax:processing', uuid); 83 | var remindTime = new Date().getTime() + config.get('FAX:retryInterval') * 1000; 84 | multi.zadd('fax:delayed', remindTime, uuid); 85 | multi.exec(function(err) { 86 | if (err) throw err; 87 | logger.debug('Fax %s suspended', uuid); 88 | sendUpdateToClients(); 89 | }); 90 | } 91 | 92 | function setFaxSuccess(uuid) { 93 | var multi = redisIO.multi(); 94 | multi.zrem('fax:processing', uuid); 95 | multi.zadd('fax:success', new Date().getTime(), uuid); 96 | multi.exec(function(err) { 97 | if (err) throw err; 98 | logger.debug('Fax %s marked as successful', uuid); 99 | sendUpdateToClients(); 100 | }); 101 | } 102 | 103 | function setFaxFailure(uuid) { 104 | var multi = redisIO.multi(); 105 | multi.zrem('fax:processing', uuid); 106 | multi.zadd('fax:failed', new Date().getTime(), uuid); 107 | multi.exec(function(err) { 108 | if (err) throw err; 109 | logger.debug('Fax %s marked as failed', uuid); 110 | sendUpdateToClients(); 111 | }); 112 | } 113 | 114 | function processDelayedFaxes() { 115 | var currentTime = new Date().getTime(); 116 | redisIO.zrangebyscore('fax:delayed','-inf',currentTime, function(err,results) { 117 | if (err) throw err; 118 | if (results.length > 0) { 119 | var multi = redisIO.multi(); 120 | for (var i=0; i