bitcore-wallet-service/lib/pushnotificationsservice.js

354 lines
9.8 KiB
JavaScript
Raw Normal View History

2016-01-04 09:43:21 -08:00
'use strict';
var _ = require('lodash');
2016-01-04 09:43:21 -08:00
var async = require('async');
2016-01-06 12:34:51 -08:00
var Mustache = require('mustache');
2016-01-04 09:43:21 -08:00
var request = require('request');
var MessageBroker = require('./messagebroker');
var Storage = require('./storage');
2016-01-06 12:34:51 -08:00
var fs = require('fs');
var path = require('path');
var Utils = require('./common/utils');
var Model = require('./model');
var log = require('npmlog');
log.debug = log.verbose;
2016-01-04 09:43:21 -08:00
2016-01-04 09:47:57 -08:00
var PUSHNOTIFICATIONS_TYPES = {
2016-01-04 09:43:21 -08:00
'NewCopayer': {
2016-01-06 12:34:51 -08:00
filename: 'new_copayer',
2016-01-04 09:43:21 -08:00
},
'WalletComplete': {
2016-01-06 12:34:51 -08:00
filename: 'wallet_complete',
2016-01-04 09:43:21 -08:00
},
'NewTxProposal': {
2016-01-06 12:34:51 -08:00
filename: 'new_tx_proposal',
2016-01-04 09:43:21 -08:00
},
'NewOutgoingTx': {
2016-01-06 12:34:51 -08:00
filename: 'new_outgoing_tx',
2016-01-04 09:43:21 -08:00
},
'NewIncomingTx': {
2016-01-06 12:34:51 -08:00
filename: 'new_incoming_tx',
2016-01-04 09:43:21 -08:00
},
'TxProposalFinallyRejected': {
2016-01-06 12:34:51 -08:00
filename: 'txp_finally_rejected',
}
2016-01-04 09:43:21 -08:00
};
2016-01-04 09:47:57 -08:00
function PushNotificationsService() {};
2016-01-04 09:43:21 -08:00
2016-01-04 09:47:57 -08:00
PushNotificationsService.prototype.start = function(opts, cb) {
2016-01-08 12:01:35 -08:00
var self = this;
2016-01-04 10:56:14 -08:00
opts = opts || {};
2016-01-06 12:34:51 -08:00
function _readDirectories(basePath, cb) {
fs.readdir(basePath, function(err, files) {
if (err) return cb(err);
async.filter(files, function(file, next) {
fs.stat(path.join(basePath, file), function(err, stats) {
return next(!err && stats.isDirectory());
});
}, function(dirs) {
return cb(null, dirs);
});
});
};
2016-01-08 12:01:35 -08:00
self.templatePath = opts.pushNotificationsOpts.templatePath || templatePathpath.normalize(((__dirname + '/templates')) + '/');
self.defaultLanguage = opts.pushNotificationsOpts.defaultLanguage || 'en';
self.defaultUnit = opts.pushNotificationsOpts.defaultUnit || 'btc';
self.subjectPrefix = opts.pushNotificationsOpts.subjectPrefix || '';
self.publicTxUrlTemplate = opts.pushNotificationsOpts.publicTxUrlTemplate || {};
self.pushServerUrl = opts.pushNotificationsOpts.pushServerUrl;
2016-01-04 10:56:14 -08:00
2016-01-04 09:43:21 -08:00
async.parallel([
function(done) {
2016-01-06 12:34:51 -08:00
_readDirectories(self.templatePath, function(err, res) {
self.availableLanguages = res;
done(err);
});
2016-01-04 09:43:21 -08:00
},
function(done) {
self.storage = new Storage();
self.storage.connect(opts.storageOpts, done);
},
2016-01-06 12:34:51 -08:00
function(done) {
self.messageBroker = opts.messageBroker || new MessageBroker(opts.messageBrokerOpts);
self.messageBroker.onMessage(_.bind(self._sendPushNotifications, self));
done();
},
2016-01-04 09:43:21 -08:00
], function(err) {
if (err) {
log.error(err);
}
return cb(err);
});
};
2016-01-06 12:34:51 -08:00
PushNotificationsService.prototype._sendPushNotifications = function(notification, cb) {
2016-01-08 12:01:35 -08:00
var self = this;
cb = cb || function() {};
2016-01-04 09:43:21 -08:00
2016-01-06 12:34:51 -08:00
var notifType = PUSHNOTIFICATIONS_TYPES[notification.type];
if (!notifType) return cb();
2016-01-05 05:53:05 -08:00
2016-01-08 12:01:35 -08:00
self._getRecipientsList(notification, function(err, recipientsList) {
if (err) return cb(err);
2016-01-06 12:34:51 -08:00
2016-01-08 12:01:35 -08:00
async.waterfall([
function(next) {
self._readAndApplyTemplates(notification, notifType, recipientsList, next);
},
function(contents, next) {
async.map(recipientsList, function(recipient, next) {
var opts = {};
var content = contents[recipient.language];
opts.users = [notification.walletId + '$' + recipient.copayerId];
opts.android = {
"data": {
"title": content.plain.subject,
"message": content.plain.body
2016-01-06 12:34:51 -08:00
}
2016-01-08 12:01:35 -08:00
};
opts.ios = {
"alert": content.plain.body,
"sound": ""
};
return next(err, opts);
}, next);
},
function(optsList, next) {
async.each(optsList,
function(opts, next) {
self._makeRequest(opts, next());
},
function(err) {
log.error(err);
return cb(err);
}
);
},
], function(err) {
if (err) {
log.error('An error ocurred generating notification', err);
}
return cb(err);
2016-01-06 12:34:51 -08:00
});
});
};
2016-01-08 12:01:35 -08:00
PushNotificationsService.prototype._getRecipientsList = function(notification, cb) {
var self = this;
2016-01-06 12:34:51 -08:00
2016-01-08 12:01:35 -08:00
self.storage.fetchWallet(notification.walletId, function(err, wallet) {
if (err) return cb(err);
2016-01-08 12:01:35 -08:00
self.storage.fetchPreferences(notification.walletId, null, function(err, preferences) {
if (err) log.error(err);
if (_.isEmpty(preferences)) preferences = [];
var recipients = _.compact(_.map(preferences, function(p) {
2016-01-06 12:34:51 -08:00
2016-01-08 12:01:35 -08:00
if (!_.contains(self.availableLanguages, p.language)) {
if (p.language)
log.warn('Language for notifications "' + p.language + '" not available.');
p.language = self.defaultLanguage;
2016-01-06 12:34:51 -08:00
}
2016-01-08 12:01:35 -08:00
return {
id: p.copayerId,
language: p.language,
unit: p.unit || self.defaultUnit,
};
}));
var recipientsList = _.reject(self._join(wallet.copayers, recipients), {
id: notification.creatorId
});
2016-01-06 12:34:51 -08:00
2016-01-08 12:01:35 -08:00
return cb(null, recipientsList);
});
2016-01-06 12:34:51 -08:00
});
};
2016-01-08 12:01:35 -08:00
PushNotificationsService.prototype._join = function(copayers, recipients) {
var self = this;
var recipientsList = _.compact(_.map(copayers, function(c) {
2016-01-06 12:34:51 -08:00
var structure = {};
2016-01-08 12:01:35 -08:00
_.forEach(recipients, function(r) {
2016-01-06 12:34:51 -08:00
if (r.id == c.id) {
2016-01-08 12:01:35 -08:00
structure.copayerId = r.id;
2016-01-06 12:34:51 -08:00
structure.language = r.language;
structure.unit = r.unit || self.defaultUnit;
}
});
2016-01-06 12:34:51 -08:00
if (_.isEmpty(structure)) {
2016-01-08 12:01:35 -08:00
structure.copayerId = c.id;
2016-01-08 08:26:51 -08:00
structure.language = self.defaultLanguage;
2016-01-06 12:34:51 -08:00
structure.unit = self.defaultUnit;
}
return structure;
}));
2016-01-08 12:01:35 -08:00
return recipientsList;
2016-01-06 12:34:51 -08:00
};
PushNotificationsService.prototype._readAndApplyTemplates = function(notification, notifType, recipientsList, cb) {
2016-01-08 12:01:35 -08:00
var self = this;
2016-01-06 12:34:51 -08:00
async.map(recipientsList, function(recipient, next) {
async.waterfall([
function(next) {
self._getDataForTemplate(notification, recipient, next);
},
function(data, next) {
async.map(['plain', 'html'], function(type, next) {
self._loadTemplate(notifType, recipient, '.' + type, function(err, template) {
if (err && type == 'html') return next();
if (err) return next(err);
self._applyTemplate(template, data, function(err, res) {
return next(err, [type, res]);
});
});
}, function(err, res) {
return next(err, _.zipObject(res));
});
},
2016-01-06 12:34:51 -08:00
function(result, next) {
next(null, result);
},
], function(err, res) {
next(err, [recipient.language, res]);
});
}, function(err, res) {
return cb(err, _.zipObject(res));
});
};
PushNotificationsService.prototype._getDataForTemplate = function(notification, recipient, cb) {
2016-01-08 12:01:35 -08:00
var self = this;
2016-01-06 12:34:51 -08:00
var UNIT_LABELS = {
btc: 'BTC',
bit: 'bits'
};
var data = _.cloneDeep(notification.data);
data.subjectPrefix = _.trim(self.subjectPrefix + ' ');
if (data.amount) {
try {
var unit = recipient.unit.toLowerCase();
data.amount = Utils.formatAmount(+data.amount, unit) + ' ' + UNIT_LABELS[unit];
} catch (ex) {
return cb(new Error('Could not format amount', ex));
}
}
self.storage.fetchWallet(notification.walletId, function(err, wallet) {
if (err) return cb(err);
data.walletId = wallet.id;
data.walletName = wallet.name;
data.walletM = wallet.m;
data.walletN = wallet.n;
var copayer = _.find(wallet.copayers, {
id: notification.creatorId
});
if (copayer) {
data.copayerId = copayer.id;
data.copayerName = copayer.name;
}
if (notification.type == 'TxProposalFinallyRejected' && data.rejectedBy) {
var rejectors = _.map(data.rejectedBy, function(copayerId) {
return _.find(wallet.copayers, {
id: copayerId
}).name
});
data.rejectorsNames = rejectors.join(', ');
}
if (_.contains(['NewIncomingTx', 'NewOutgoingTx'], notification.type) && data.txid) {
var urlTemplate = self.publicTxUrlTemplate[wallet.network];
if (urlTemplate) {
try {
data.urlForTx = Mustache.render(urlTemplate, data);
} catch (ex) {
log.warn('Could not render public url for tx', ex);
}
}
2016-01-06 12:34:51 -08:00
}
return cb(null, data);
});
};
PushNotificationsService.prototype._applyTemplate = function(template, data, cb) {
if (!data) return cb(new Error('Could not apply template to empty data'));
var error;
var result = _.mapValues(template, function(t) {
try {
return Mustache.render(t, data);
} catch (e) {
log.error('Could not apply data to template', e);
error = e;
}
});
if (error) return cb(error);
return cb(null, result);
};
PushNotificationsService.prototype._loadTemplate = function(notifType, recipient, extension, cb) {
2016-01-08 12:01:35 -08:00
var self = this;
2016-01-06 12:34:51 -08:00
self._readTemplateFile(recipient.language, notifType.filename + extension, function(err, template) {
if (err) return cb(err);
return cb(null, self._compileTemplate(template, extension));
});
};
PushNotificationsService.prototype._readTemplateFile = function(language, filename, cb) {
2016-01-08 12:01:35 -08:00
var self = this;
2016-01-06 12:34:51 -08:00
var fullFilename = path.join(self.templatePath, language, filename);
fs.readFile(fullFilename, 'utf8', function(err, template) {
if (err) {
return cb(new Error('Could not read template file ' + fullFilename, err));
}
return cb(null, template);
2016-01-05 05:53:05 -08:00
});
2016-01-04 09:43:21 -08:00
};
2016-01-06 12:34:51 -08:00
PushNotificationsService.prototype._compileTemplate = function(template, extension) {
var lines = template.split('\n');
if (extension == '.html') {
lines.unshift('');
}
return {
subject: lines[0],
body: _.rest(lines).join('\n'),
};
};
2016-01-08 12:01:35 -08:00
PushNotificationsService.prototype._makeRequest = function(opts, cb) {
var self = this;
2016-01-08 08:26:51 -08:00
request({
2016-01-08 12:01:35 -08:00
url: self.pushServerUrl,
2016-01-08 08:26:51 -08:00
method: 'POST',
json: true,
body: opts
}, function(error, response) {
2016-01-08 12:01:35 -08:00
if (error) log.error(error);
log.debug('Post status : ', response);
return;
2016-01-08 08:26:51 -08:00
});
};
2016-01-04 09:47:57 -08:00
module.exports = PushNotificationsService;