123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- 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;
|