var Constants = require('./constants'); var _ = require('lodash'); var request = require('request'); var debug = require('debug')('node-gcm'); function Sender(key, options) { if (!(this instanceof Sender)) { return new Sender(key, options); } this.key = key; this.options = options || {}; } Sender.prototype.send = function(message, recipient, options, callback) { if(typeof options == "function") { callback = options; options = null; } else if(!callback) { callback = function() {}; } options = cleanOptions(options); if(message.params && message.params.data && message.params.data.from) { console.warn("Sending a notification with the 'from' data attribute may invoke a 400 Bad Request by FCM."); } if(options.retries == 0) { return this.sendNoRetry(message, recipient, callback); } var self = this; this.sendNoRetry(message, recipient, function(err, response, attemptedRegTokens) { if (err) { // Attempt to determine HTTP status code var statusCode = typeof err === 'number' ? err : (err.code || 0); // 4xx error? if (statusCode > 399 && statusCode < 500) { debug("Error 4xx -- no use retrying. Something is wrong with the request (probably authentication?)"); return callback(err); } return retry(self, message, recipient, options, callback); } if(!response.results) { return callback(null, response); } checkForBadTokens(response.results, attemptedRegTokens, function(err, unsentRegTokens, regTokenPositionMap) { if(err) { return callback(err); } if (unsentRegTokens.length == 0) { return callback(null, response); } debug("Retrying " + unsentRegTokens.length + " unsent registration tokens"); retry(self, message, unsentRegTokens, options, function(err, retriedResponse) { if(err) { return callback(null, response); } response = updateResponse(response, retriedResponse, regTokenPositionMap, unsentRegTokens); callback(null, response); }); }); }); }; function cleanOptions(options) { if(!options || typeof options != "object") { var retries = 5; if(typeof options == "number") { retries = options; } return { retries: retries, backoff: Constants.BACKOFF_INITIAL_DELAY }; } if(typeof options.retries != "number") { options.retries = 5; } if(typeof options.backoff != "number") { options.backoff = Constants.BACKOFF_INITIAL_DELAY; } if (options.backoff > Constants.MAX_BACKOFF_DELAY) { options.backoff = Constants.MAX_BACKOFF_DELAY; } return options; } function retry(self, message, recipient, options, callback) { return setTimeout(function() { self.send(message, recipient, { retries: options.retries - 1, backoff: options.backoff * 2 }, callback); }, options.backoff); } function checkForBadTokens(results, originalRecipients, callback) { var unsentRegTokens = []; var regTokenPositionMap = []; for (var i = 0; i < results.length; i++) { if (results[i].error === 'Unavailable' || results[i].error === 'InternalServerError') { regTokenPositionMap.push(i); unsentRegTokens.push(originalRecipients[i]); } } nextTick(callback, null, unsentRegTokens, regTokenPositionMap); } function updateResponse(response, retriedResponse, regTokenPositionMap, unsentRegTokens) { updateResults(response.results, retriedResponse.results, regTokenPositionMap); updateResponseMetaData(response, retriedResponse, unsentRegTokens); return response; } function updateResults(results, retriedResults, regTokenPositionMap) { for(var i = 0; i < results.length; i++) { results[regTokenPositionMap[i]] = retriedResults[i]; } } function updateResponseMetaData(response, retriedResponse, unsentRegTokens) { response.success += retriedResponse.success; response.canonical_ids += retriedResponse.canonical_ids; response.failure -= unsentRegTokens.length - retriedResponse.failure; } Sender.prototype.sendNoRetry = function(message, recipient, callback) { if(!callback) { callback = function() {}; } getRequestBody(message, recipient, function(err, body) { if(err) { return callback(err); } //Build request options, allowing some to be overridden var request_options = _.defaultsDeep({ method: 'POST', headers: { 'Authorization': 'key=' + this.key }, uri: Constants.GCM_SEND_URI, json: body }, this.options, { timeout: Constants.SOCKET_TIMEOUT }); request(request_options, function (err, res, resBodyJSON) { if (err) { return callback(err); } if (res.statusCode >= 500) { debug('GCM service is unavailable (500)'); return callback(res.statusCode); } if (res.statusCode === 401) { debug('Unauthorized (401). Check that your API token is correct.'); return callback(res.statusCode); } if (res.statusCode !== 200) { debug('Invalid request (' + res.statusCode + '): ' + resBodyJSON); return callback(res.statusCode); } if (!resBodyJSON) { debug('Empty response received (' + res.statusCode + ' ' + res.statusMessage + ')'); // Spoof error code 400 to avoid retrying the request return callback({error: res.statusMessage, code: 400}); } callback(null, resBodyJSON, body.registration_ids || [ body.to ]); }); }.bind(this)); }; function getRequestBody(message, recipient, callback) { var body = message.toJson(); if(typeof recipient == "string") { body.to = recipient; return nextTick(callback, null, body); } if(Array.isArray(recipient)) { if(!recipient.length) { return nextTick(callback, 'No recipient provided!'); } else if(recipient.length == 1) { body.to = recipient[0]; return nextTick(callback, null, body); } body.registration_ids = recipient; return nextTick(callback, null, body); } if (typeof recipient == "object") { return extractRecipient(recipient, function(err, recipient, param) { if(err) { return callback(err); } body[param] = recipient; return callback(null, body); }); } return nextTick(callback, 'Invalid recipient (' + recipient + ', type ' + typeof recipient + ') provided!'); } function nextTick(func) { var args = Array.prototype.slice.call(arguments, 1); process.nextTick(function() { func.apply(this, args); }.bind(this)); } function extractRecipient(recipient, callback) { var recipientKeys = Object.keys(recipient); if(recipientKeys.length !== 1) { return nextTick(callback, new Error("Please specify exactly one recipient key (you specified [" + recipientKeys + "])")); } var key = recipientKeys[0]; var value = recipient[key]; if(!value) { return nextTick(callback, new Error("Falsy value for recipient key '" + key + "'.")); } var keyValidators = { to: isString, topic: isString, condition: isString, notificationKey: isString, registrationIds: isArray, registrationTokens: isArray }; var validator = keyValidators[key]; if(!validator) { return nextTick(callback, new Error("Key '" + key + "' is not a valid recipient key.")); } if(!validator(value)) { return nextTick(callback, new Error("Recipient key '" + key + "' was provided as an incorrect type.")); } var param = getParamFromKey(key); return nextTick(callback, null, value, param); } function getParamFromKey(key) { if (key === 'condition') { return 'condition'; } else if (['registrationIds', 'registrationTokens'].indexOf(key) !== -1) { return 'registration_ids'; } else { return 'to'; } } function isString(x) { return typeof x == "string"; } function isArray(x) { return Array.isArray(x); } module.exports = Sender;