From 81a5a8f0db9c0ed470bb0a9ac7b7aff26b447da2 Mon Sep 17 00:00:00 2001 From: wangmengyan95 Date: Thu, 4 Feb 2016 00:08:31 -0800 Subject: [PATCH] Add APNS client --- APNS.js | 95 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- spec/APNS.spec.js | 58 +++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 APNS.js create mode 100644 spec/APNS.spec.js diff --git a/APNS.js b/APNS.js new file mode 100644 index 0000000000..5fc73ab0f2 --- /dev/null +++ b/APNS.js @@ -0,0 +1,95 @@ +var Parse = require('parse/node').Parse; +// TODO: apn does not support the new HTTP/2 protocal. It is fine to use it in V1, +// but probably we will replace it in the future. +var apn = require('apn'); + +/** + * Create a new connection to the APN service. + * @constructor + * @param {Object} args Arguments to config APNS connection + * @param {String} args.cert The filename of the connection certificate to load from disk, default is cert.pem + * @param {String} args.key The filename of the connection key to load from disk, default is key.pem + * @param {String} args.passphrase The passphrase for the connection key, if required + * @param {Boolean} args.production Specifies which environment to connect to: Production (if true) or Sandbox + */ +function APNS(args) { + this.sender = new apn.connection(args); + + this.sender.on('connected', function() { + console.log('APNS Connected'); + }); + + this.sender.on('transmissionError', function(errCode, notification, device) { + console.error('APNS Notification caused error: ' + errCode + ' for device ', device, notification); + // TODO: For error caseud by invalid deviceToken, we should mark those installations. + }); + + this.sender.on("timeout", function () { + console.log("APNS Connection Timeout"); + }); + + this.sender.on("disconnected", function() { + console.log("APNS Disconnected"); + }); + + this.sender.on("socketError", console.error); +} + +/** + * Send apns request. + * @param {Object} data The data we need to send, the format is the same with api request body + * @param {Array} deviceTokens A array of device tokens + * @returns {Object} A promise which is resolved immediately + */ +APNS.prototype.send = function(data, deviceTokens) { + var coreData = data.data; + var expirationTime = data['expiration_time']; + var notification = generateNotification(coreData, expirationTime); + this.sender.pushNotification(notification, deviceTokens); + // TODO: pushNotification will push the notification to apn's queue. + // We do not handle error in V1, we just relies apn to auto retry and send the + // notifications. + return Parse.Promise.as(); +} + +/** + * Generate the apns notification from the data we get from api request. + * @param {Object} coreData The data field under api request body + * @returns {Object} A apns notification + */ +var generateNotification = function(coreData, expirationTime) { + var notification = new apn.notification(); + var payload = {}; + for (key in coreData) { + switch (key) { + case 'alert': + notification.setAlertText(coreData.alert); + break; + case 'badge': + notification.badge = coreData.badge; + break; + case 'sound': + notification.sound = coreData.sound; + break; + case 'content-available': + notification.setNewsstandAvailable(true); + var isAvailable = coreData['content-available'] === 1; + notification.setContentAvailable(isAvailable); + break; + case 'category': + notification.category = coreData.category; + break; + default: + payload[key] = coreData[key]; + break; + } + } + notification.payload = payload; + notification.expiry = expirationTime; + return notification; +} + +if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') { + APNS.generateNotification = generateNotification; +} +module.exports = APNS; diff --git a/package.json b/package.json index b6039e572e..29afd4e0ea 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "license": "BSD-3-Clause", "dependencies": { + "apn": "^1.7.5", "aws-sdk": "~2.2.33", "bcrypt-nodejs": "0.0.3", "body-parser": "^1.14.2", @@ -30,7 +31,7 @@ }, "scripts": { "pretest": "MONGODB_VERSION=${MONGODB_VERSION:=3.0.8} mongodb-runner start", - "test": "TESTING=1 ./node_modules/.bin/istanbul cover --include-all-sources -x **/spec/** ./node_modules/.bin/jasmine", + "test": "NODE_ENV=test TESTING=1 ./node_modules/.bin/istanbul cover --include-all-sources -x **/spec/** ./node_modules/.bin/jasmine", "posttest": "mongodb-runner stop", "start": "./bin/parse-server" }, diff --git a/spec/APNS.spec.js b/spec/APNS.spec.js new file mode 100644 index 0000000000..c50bb5c952 --- /dev/null +++ b/spec/APNS.spec.js @@ -0,0 +1,58 @@ +var APNS = require('../APNS'); + +describe('APNS', () => { + it('can generate APNS notification', (done) => { + //Mock request data + var data = { + 'alert': 'alert', + 'badge': 100, + 'sound': 'test', + 'content-available': 1, + 'category': 'INVITE_CATEGORY', + 'key': 'value', + 'keyAgain': 'valueAgain' + }; + var expirationTime = 1454571491354 + + var notification = APNS.generateNotification(data, expirationTime); + + expect(notification.alert).toEqual(data.alert); + expect(notification.badge).toEqual(data.badge); + expect(notification.sound).toEqual(data.sound); + expect(notification.contentAvailable).toEqual(1); + expect(notification.category).toEqual(data.category); + expect(notification.payload).toEqual({ + 'key': 'value', + 'keyAgain': 'valueAgain' + }); + expect(notification.expiry).toEqual(expirationTime); + done(); + }); + + it('can send APNS notification', (done) => { + var apns = new APNS(); + var sender = { + pushNotification: jasmine.createSpy('send') + }; + apns.sender = sender; + // Mock data + var expirationTime = 1454571491354 + var data = { + 'expiration_time': expirationTime, + 'data': { + 'alert': 'alert' + } + } + // Mock registrationTokens + var deviceTokens = ['token']; + + var promise = apns.send(data, deviceTokens); + expect(sender.pushNotification).toHaveBeenCalled(); + var args = sender.pushNotification.calls.first().args; + var notification = args[0]; + expect(notification.alert).toEqual(data.data.alert); + expect(notification.expiry).toEqual(data['expiration_time']); + expect(args[1]).toEqual(deviceTokens); + done(); + }); +});