|
|
@ -2,48 +2,35 @@ |
|
|
|
|
|
|
|
var _ = require('lodash'); |
|
|
|
var async = require('async'); |
|
|
|
var log = require('npmlog'); |
|
|
|
log.debug = log.verbose; |
|
|
|
var Mustache = require('mustache'); |
|
|
|
var request = require('request'); |
|
|
|
var MessageBroker = require('./messagebroker'); |
|
|
|
var Storage = require('./storage'); |
|
|
|
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; |
|
|
|
|
|
|
|
var PUSHNOTIFICATIONS_TYPES = { |
|
|
|
'NewCopayer': { |
|
|
|
title: "New copayer", |
|
|
|
message: function(notification) { |
|
|
|
return ("Copayer: " + notification.data.copayerName + " has joined!"); |
|
|
|
} |
|
|
|
filename: 'new_copayer', |
|
|
|
}, |
|
|
|
'WalletComplete': { |
|
|
|
title: "Wallet complete", |
|
|
|
message: function(notification) { |
|
|
|
return ("All copayers has joined!"); |
|
|
|
} |
|
|
|
filename: 'wallet_complete', |
|
|
|
}, |
|
|
|
'NewTxProposal': { |
|
|
|
title: "New proposal", |
|
|
|
message: function(notification) { |
|
|
|
return ("New transaction proposal created"); |
|
|
|
} |
|
|
|
filename: 'new_tx_proposal', |
|
|
|
}, |
|
|
|
'NewOutgoingTx': { |
|
|
|
title: "New outgoing transaction", |
|
|
|
message: function(notification) { |
|
|
|
return ((notification.data.amount / 100) + " bits"); |
|
|
|
} |
|
|
|
filename: 'new_outgoing_tx', |
|
|
|
}, |
|
|
|
'NewIncomingTx': { |
|
|
|
title: "New incoming transaction", |
|
|
|
message: function(notification) { |
|
|
|
return ((notification.data.amount / 100) + " bits"); |
|
|
|
} |
|
|
|
filename: 'new_incoming_tx', |
|
|
|
}, |
|
|
|
'TxProposalFinallyRejected': { |
|
|
|
title: "Rejected", |
|
|
|
message: function(notification) { |
|
|
|
return ("Transaction proposal finally rejected"); |
|
|
|
} |
|
|
|
filename: 'txp_finally_rejected', |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
@ -52,18 +39,43 @@ function PushNotificationsService() {}; |
|
|
|
PushNotificationsService.prototype.start = function(opts, cb) { |
|
|
|
|
|
|
|
opts = opts || {}; |
|
|
|
|
|
|
|
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); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
var self = this; |
|
|
|
self.templatePath = path.normalize(((__dirname + '/templates')) + '/'); |
|
|
|
self.defaultLanguage = 'en'; |
|
|
|
self.defaultUnit = 'btc'; |
|
|
|
self.subjectPrefix = ''; |
|
|
|
self.publicTxUrlTemplate = {}; |
|
|
|
|
|
|
|
async.parallel([ |
|
|
|
function(done) { |
|
|
|
self.messageBroker = opts.messageBroker || new MessageBroker(opts.messageBrokerOpts); |
|
|
|
self.messageBroker.onMessage(_.bind(self.sendPushNotifications, self)); |
|
|
|
done(); |
|
|
|
_readDirectories(self.templatePath, function(err, res) { |
|
|
|
self.availableLanguages = res; |
|
|
|
done(err); |
|
|
|
}); |
|
|
|
}, |
|
|
|
function(done) { |
|
|
|
self.storage = new Storage(); |
|
|
|
self.storage.connect(opts.storageOpts, done); |
|
|
|
}, |
|
|
|
function(done) { |
|
|
|
self.messageBroker = opts.messageBroker || new MessageBroker(opts.messageBrokerOpts); |
|
|
|
self.messageBroker.onMessage(_.bind(self._sendPushNotifications, self)); |
|
|
|
done(); |
|
|
|
}, |
|
|
|
], function(err) { |
|
|
|
if (err) { |
|
|
|
log.error(err); |
|
|
@ -72,49 +84,263 @@ PushNotificationsService.prototype.start = function(opts, cb) { |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
PushNotificationsService.prototype.sendPushNotifications = function(notification, cb) { |
|
|
|
PushNotificationsService.prototype._sendPushNotifications = function(notification, cb) { |
|
|
|
var self = this; |
|
|
|
var url = 'http://192.168.1.128:8000/send'; |
|
|
|
cb = cb || function() {}; |
|
|
|
if (!PUSHNOTIFICATIONS_TYPES[notification.type]) return cb(); |
|
|
|
|
|
|
|
console.log(notification); |
|
|
|
var notifType = PUSHNOTIFICATIONS_TYPES[notification.type]; |
|
|
|
if (!notifType) return cb(); |
|
|
|
|
|
|
|
self.storage.fetchWallet(notification.walletId, function(err, wallet) { |
|
|
|
self._getRecipientsList(notification.walletId, function(err, recipientsList) { |
|
|
|
self.storage.fetchWallet(notification.walletId, function(err, wallet) { |
|
|
|
|
|
|
|
var resultedRecipientsList = _.reject(self._getJoinedRecipientsList(wallet, recipientsList), { |
|
|
|
id: notification.creatorId || null |
|
|
|
}); |
|
|
|
|
|
|
|
async.waterfall([ |
|
|
|
function(next) { |
|
|
|
self._readAndApplyTemplates(notification, notifType, resultedRecipientsList, next); |
|
|
|
}, |
|
|
|
function(contents, next) { |
|
|
|
async.map(resultedRecipientsList, function(recipient, next) { |
|
|
|
var opts = {}; |
|
|
|
var content = contents[recipient.language]; |
|
|
|
opts.users = [notification.walletId + '$' + recipient.id]; |
|
|
|
opts.android = { |
|
|
|
"data": { |
|
|
|
"title": content.plain.subject, |
|
|
|
"message": content.plain.body |
|
|
|
} |
|
|
|
}; |
|
|
|
return next(err, opts); |
|
|
|
}, next); |
|
|
|
}, |
|
|
|
function(optsList, next) { |
|
|
|
async.each(optsList, |
|
|
|
function(opts, next) { |
|
|
|
request({ |
|
|
|
url: url, |
|
|
|
method: 'POST', |
|
|
|
json: true, |
|
|
|
body: opts |
|
|
|
}, function(error, response, body) { |
|
|
|
next(); |
|
|
|
}); |
|
|
|
}, |
|
|
|
function(err) { |
|
|
|
log.error(err); |
|
|
|
return cb(err); |
|
|
|
} |
|
|
|
); |
|
|
|
}, |
|
|
|
], function(err) { |
|
|
|
if (err) { |
|
|
|
log.error('An error ocurred generating notification', err); |
|
|
|
} |
|
|
|
return cb(err); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
PushNotificationsService.prototype._getRecipientsList = function(walletId, cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
self.storage.fetchPreferences(walletId, null, function(err, preferences) { |
|
|
|
if (err) return cb(err); |
|
|
|
if (_.isEmpty(preferences)) return cb(null, []); |
|
|
|
|
|
|
|
var copayers = _.reject(wallet.copayers, { |
|
|
|
id: notification.creatorId |
|
|
|
var recipients = _.compact(_.map(preferences, function(p) { |
|
|
|
|
|
|
|
if (!_.contains(self.availableLanguages, p.language)) { |
|
|
|
if (!p.language) { |
|
|
|
log.warn('Language for notifications "' + p.language + '" not available.'); |
|
|
|
p.language = self.defaultLanguage; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return { |
|
|
|
id: p.copayerId, |
|
|
|
language: p.language, |
|
|
|
unit: p.unit || self.defaultUnit, |
|
|
|
}; |
|
|
|
})); |
|
|
|
|
|
|
|
return cb(null, recipients); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
PushNotificationsService.prototype._getJoinedRecipientsList = function(wallet, recipientsList) { |
|
|
|
var self = this; |
|
|
|
var _recipientsList = _.compact(_.map(wallet.copayers, function(c) { |
|
|
|
|
|
|
|
if (!recipientsList) return { |
|
|
|
id: c.id, |
|
|
|
language: 'en', |
|
|
|
unit: self.defaultUnit |
|
|
|
}; |
|
|
|
|
|
|
|
var structure = {}; |
|
|
|
|
|
|
|
_.forEach(recipientsList, function(r) { |
|
|
|
if (r.id == c.id) { |
|
|
|
structure.id = r.id; |
|
|
|
structure.language = r.language; |
|
|
|
structure.unit = r.unit || self.defaultUnit; |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
var url = 'http://192.168.1.121:8000/send'; |
|
|
|
var opts = {}; |
|
|
|
|
|
|
|
async.each(copayers, |
|
|
|
function(c, next) { |
|
|
|
opts.users = [notification.walletId + '$' + c.id]; |
|
|
|
opts.android = { |
|
|
|
"data": { |
|
|
|
"title": PUSHNOTIFICATIONS_TYPES[notification.type].title, |
|
|
|
"message": PUSHNOTIFICATIONS_TYPES[notification.type].message(notification) |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
request({ |
|
|
|
url: url, |
|
|
|
method: 'POST', |
|
|
|
json: true, |
|
|
|
body: opts |
|
|
|
}, function(error, response, body) { |
|
|
|
console.log(response.statusCode); |
|
|
|
next(); |
|
|
|
if (_.isEmpty(structure)) { |
|
|
|
structure.id = c.id; |
|
|
|
structure.language = 'en'; |
|
|
|
structure.unit = self.defaultUnit; |
|
|
|
} |
|
|
|
|
|
|
|
return structure; |
|
|
|
})); |
|
|
|
return _recipientsList; |
|
|
|
}; |
|
|
|
|
|
|
|
PushNotificationsService.prototype._readAndApplyTemplates = function(notification, notifType, recipientsList, cb) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
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)); |
|
|
|
}); |
|
|
|
}, |
|
|
|
function(err) { |
|
|
|
log.error(err); |
|
|
|
return cb(err); |
|
|
|
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) { |
|
|
|
var self = this; |
|
|
|
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); |
|
|
|
} |
|
|
|
} |
|
|
|
); |
|
|
|
} |
|
|
|
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) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
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) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
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); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
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'), |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
module.exports = PushNotificationsService; |
|
|
|