sender.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. var Constants = require('./constants');
  2. var _ = require('lodash');
  3. var request = require('request');
  4. var debug = require('debug')('node-gcm');
  5. function Sender(key, options) {
  6. if (!(this instanceof Sender)) {
  7. return new Sender(key, options);
  8. }
  9. this.key = key;
  10. this.options = options || {};
  11. }
  12. Sender.prototype.send = function(message, recipient, options, callback) {
  13. if(typeof options == "function") {
  14. callback = options;
  15. options = null;
  16. }
  17. else if(!callback) {
  18. callback = function() {};
  19. }
  20. options = cleanOptions(options);
  21. if(message.params && message.params.data && message.params.data.from) {
  22. console.warn("Sending a notification with the 'from' data attribute may invoke a 400 Bad Request by FCM.");
  23. }
  24. if(options.retries == 0) {
  25. return this.sendNoRetry(message, recipient, callback);
  26. }
  27. var self = this;
  28. this.sendNoRetry(message, recipient, function(err, response, attemptedRegTokens) {
  29. if (err) {
  30. // Attempt to determine HTTP status code
  31. var statusCode = typeof err === 'number' ? err : (err.code || 0);
  32. // 4xx error?
  33. if (statusCode > 399 && statusCode < 500) {
  34. debug("Error 4xx -- no use retrying. Something is wrong with the request (probably authentication?)");
  35. return callback(err);
  36. }
  37. return retry(self, message, recipient, options, callback);
  38. }
  39. if(!response.results) {
  40. return callback(null, response);
  41. }
  42. checkForBadTokens(response.results, attemptedRegTokens, function(err, unsentRegTokens, regTokenPositionMap) {
  43. if(err) {
  44. return callback(err);
  45. }
  46. if (unsentRegTokens.length == 0) {
  47. return callback(null, response);
  48. }
  49. debug("Retrying " + unsentRegTokens.length + " unsent registration tokens");
  50. retry(self, message, unsentRegTokens, options, function(err, retriedResponse) {
  51. if(err) {
  52. return callback(null, response);
  53. }
  54. response = updateResponse(response, retriedResponse, regTokenPositionMap, unsentRegTokens);
  55. callback(null, response);
  56. });
  57. });
  58. });
  59. };
  60. function cleanOptions(options) {
  61. if(!options || typeof options != "object") {
  62. var retries = 5;
  63. if(typeof options == "number") {
  64. retries = options;
  65. }
  66. return {
  67. retries: retries,
  68. backoff: Constants.BACKOFF_INITIAL_DELAY
  69. };
  70. }
  71. if(typeof options.retries != "number") {
  72. options.retries = 5;
  73. }
  74. if(typeof options.backoff != "number") {
  75. options.backoff = Constants.BACKOFF_INITIAL_DELAY;
  76. }
  77. if (options.backoff > Constants.MAX_BACKOFF_DELAY) {
  78. options.backoff = Constants.MAX_BACKOFF_DELAY;
  79. }
  80. return options;
  81. }
  82. function retry(self, message, recipient, options, callback) {
  83. return setTimeout(function() {
  84. self.send(message, recipient, {
  85. retries: options.retries - 1,
  86. backoff: options.backoff * 2
  87. }, callback);
  88. }, options.backoff);
  89. }
  90. function checkForBadTokens(results, originalRecipients, callback) {
  91. var unsentRegTokens = [];
  92. var regTokenPositionMap = [];
  93. for (var i = 0; i < results.length; i++) {
  94. if (results[i].error === 'Unavailable' || results[i].error === 'InternalServerError') {
  95. regTokenPositionMap.push(i);
  96. unsentRegTokens.push(originalRecipients[i]);
  97. }
  98. }
  99. nextTick(callback, null, unsentRegTokens, regTokenPositionMap);
  100. }
  101. function updateResponse(response, retriedResponse, regTokenPositionMap, unsentRegTokens) {
  102. updateResults(response.results, retriedResponse.results, regTokenPositionMap);
  103. updateResponseMetaData(response, retriedResponse, unsentRegTokens);
  104. return response;
  105. }
  106. function updateResults(results, retriedResults, regTokenPositionMap) {
  107. for(var i = 0; i < results.length; i++) {
  108. results[regTokenPositionMap[i]] = retriedResults[i];
  109. }
  110. }
  111. function updateResponseMetaData(response, retriedResponse, unsentRegTokens) {
  112. response.success += retriedResponse.success;
  113. response.canonical_ids += retriedResponse.canonical_ids;
  114. response.failure -= unsentRegTokens.length - retriedResponse.failure;
  115. }
  116. Sender.prototype.sendNoRetry = function(message, recipient, callback) {
  117. if(!callback) {
  118. callback = function() {};
  119. }
  120. getRequestBody(message, recipient, function(err, body) {
  121. if(err) {
  122. return callback(err);
  123. }
  124. //Build request options, allowing some to be overridden
  125. var request_options = _.defaultsDeep({
  126. method: 'POST',
  127. headers: {
  128. 'Authorization': 'key=' + this.key
  129. },
  130. uri: Constants.GCM_SEND_URI,
  131. json: body
  132. }, this.options, {
  133. timeout: Constants.SOCKET_TIMEOUT
  134. });
  135. request(request_options, function (err, res, resBodyJSON) {
  136. if (err) {
  137. return callback(err);
  138. }
  139. if (res.statusCode >= 500) {
  140. debug('GCM service is unavailable (500)');
  141. return callback(res.statusCode);
  142. }
  143. if (res.statusCode === 401) {
  144. debug('Unauthorized (401). Check that your API token is correct.');
  145. return callback(res.statusCode);
  146. }
  147. if (res.statusCode !== 200) {
  148. debug('Invalid request (' + res.statusCode + '): ' + resBodyJSON);
  149. return callback(res.statusCode);
  150. }
  151. if (!resBodyJSON) {
  152. debug('Empty response received (' + res.statusCode + ' ' + res.statusMessage + ')');
  153. // Spoof error code 400 to avoid retrying the request
  154. return callback({error: res.statusMessage, code: 400});
  155. }
  156. callback(null, resBodyJSON, body.registration_ids || [ body.to ]);
  157. });
  158. }.bind(this));
  159. };
  160. function getRequestBody(message, recipient, callback) {
  161. var body = message.toJson();
  162. if(typeof recipient == "string") {
  163. body.to = recipient;
  164. return nextTick(callback, null, body);
  165. }
  166. if(Array.isArray(recipient)) {
  167. if(!recipient.length) {
  168. return nextTick(callback, 'No recipient provided!');
  169. }
  170. else if(recipient.length == 1) {
  171. body.to = recipient[0];
  172. return nextTick(callback, null, body);
  173. }
  174. body.registration_ids = recipient;
  175. return nextTick(callback, null, body);
  176. }
  177. if (typeof recipient == "object") {
  178. return extractRecipient(recipient, function(err, recipient, param) {
  179. if(err) {
  180. return callback(err);
  181. }
  182. body[param] = recipient;
  183. return callback(null, body);
  184. });
  185. }
  186. return nextTick(callback, 'Invalid recipient (' + recipient + ', type ' + typeof recipient + ') provided!');
  187. }
  188. function nextTick(func) {
  189. var args = Array.prototype.slice.call(arguments, 1);
  190. process.nextTick(function() {
  191. func.apply(this, args);
  192. }.bind(this));
  193. }
  194. function extractRecipient(recipient, callback) {
  195. var recipientKeys = Object.keys(recipient);
  196. if(recipientKeys.length !== 1) {
  197. return nextTick(callback, new Error("Please specify exactly one recipient key (you specified [" + recipientKeys + "])"));
  198. }
  199. var key = recipientKeys[0];
  200. var value = recipient[key];
  201. if(!value) {
  202. return nextTick(callback, new Error("Falsy value for recipient key '" + key + "'."));
  203. }
  204. var keyValidators = {
  205. to: isString,
  206. topic: isString,
  207. condition: isString,
  208. notificationKey: isString,
  209. registrationIds: isArray,
  210. registrationTokens: isArray
  211. };
  212. var validator = keyValidators[key];
  213. if(!validator) {
  214. return nextTick(callback, new Error("Key '" + key + "' is not a valid recipient key."));
  215. }
  216. if(!validator(value)) {
  217. return nextTick(callback, new Error("Recipient key '" + key + "' was provided as an incorrect type."));
  218. }
  219. var param = getParamFromKey(key);
  220. return nextTick(callback, null, value, param);
  221. }
  222. function getParamFromKey(key) {
  223. if (key === 'condition') {
  224. return 'condition';
  225. }
  226. else if (['registrationIds', 'registrationTokens'].indexOf(key) !== -1) {
  227. return 'registration_ids';
  228. }
  229. else {
  230. return 'to';
  231. }
  232. }
  233. function isString(x) {
  234. return typeof x == "string";
  235. }
  236. function isArray(x) {
  237. return Array.isArray(x);
  238. }
  239. module.exports = Sender;