From a1b24da3e7e73341d004cddb371c67cf7250c36b Mon Sep 17 00:00:00 2001 From: George Deglin Date: Fri, 12 Feb 2016 18:32:39 -0800 Subject: [PATCH] WIP Add OneSignal Adapter --- spec/OneSignalPushAdapter.spec.js | 1 + spec/ParseInstallation.spec.js | 20 --- src/Adapters/Push/OneSignalPushAdapter.js | 209 ++++++++++++++++++++++ src/RestWrite.js | 5 - 4 files changed, 210 insertions(+), 25 deletions(-) create mode 100644 spec/OneSignalPushAdapter.spec.js create mode 100644 src/Adapters/Push/OneSignalPushAdapter.js diff --git a/spec/OneSignalPushAdapter.spec.js b/spec/OneSignalPushAdapter.spec.js new file mode 100644 index 00000000..8371f3a2 --- /dev/null +++ b/spec/OneSignalPushAdapter.spec.js @@ -0,0 +1 @@ +//todo \ No newline at end of file diff --git a/spec/ParseInstallation.spec.js b/spec/ParseInstallation.spec.js index 91bb9a23..cef6871e 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -133,26 +133,6 @@ describe('Installations', () => { }); }); - it('fails for android with device token', (done) => { - var installId = '12345678-abcd-abcd-abcd-123456789abc'; - var t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306'; - var device = 'android'; - var input = { - 'installationId': installId, - 'deviceType': device, - 'deviceToken': t, - 'channels': ['foo', 'bar'] - }; - rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => { - fail('Should not have been able to create an Installation.'); - done(); - }).catch((error) => { - expect(error.code).toEqual(114); - done(); - }); - }); - it('fails for android with missing type', (done) => { var installId = '12345678-abcd-abcd-abcd-123456789abc'; var input = { diff --git a/src/Adapters/Push/OneSignalPushAdapter.js b/src/Adapters/Push/OneSignalPushAdapter.js new file mode 100644 index 00000000..73f61ce8 --- /dev/null +++ b/src/Adapters/Push/OneSignalPushAdapter.js @@ -0,0 +1,209 @@ +"use strict"; +// ParsePushAdapter is the default implementation of +// PushAdapter, it uses GCM for android push and APNS +// for ios push. + +const Parse = require('parse/node').Parse; + +function OneSignalPushAdapter(pushConfig) { + this.https = require('https'); + + this.validPushTypes = ['ios', 'gcm','android']; + this.senderMap = {}; + + pushConfig = pushConfig || {}; + this.OneSignalConfig = {}; + this.OneSignalConfig['appId'] = pushConfig['oneSignalAppId']; + this.OneSignalConfig['apiKey'] = pushConfig['oneSignalApiKey']; + + this.senderMap['ios'] = this.sendToAPNS.bind(this); + this.senderMap['gcm'] = this.sendToGCM.bind(this); + this.senderMap['android'] = this.sendToGCM.bind(this); +} + +/** + * Get an array of valid push types. + * @returns {Array} An array of valid push types + */ +OneSignalPushAdapter.prototype.getValidPushTypes = function() { + return this.validPushTypes; +} + +OneSignalPushAdapter.prototype.send = function(data, installations) { + let deviceMap = classifyInstallation(installations, this.validPushTypes); + + let sendPromises = []; + for (let pushType in deviceMap) { + let sender = this.senderMap[pushType]; + if (!sender) { + console.log('Can not find sender for push type %s, %j', pushType, data); + continue; + } + let devices = deviceMap[pushType]; + + if(devices.length > 0) { + sendPromises.push(sender(data, devices)); + } + } + return Parse.Promise.when(sendPromises); +} + +OneSignalPushAdapter.prototype.sendToAPNS = function(data,tokens) { + + let post = {}; + if(data['badge']) { + if(data['badge'] == "Increment") { + post['ios_badgeType'] = 'Increase'; + post['ios_badgeCount'] = 1; + } else { + post['ios_badgeType'] = 'SetTo'; + post['ios_badgeCount'] = data['badge']; + } + delete data['badge']; + } + if(data['alert']) { + post['contents'] = {en: data['alert']}; + delete data['alert']; + } + if(data['sound']) { + post['ios_sound'] = data['sound']; + delete data['sound']; + } + if(data['content-available'] == 1) { + post['content_available'] = true; + delete data['content-available']; + } + post['data'] = data; + + let promise = new Parse.Promise(); + + var chunk = 2000 // OneSignal can process 2000 devices at a time + var tokenlength=tokens.length; + var offset = 0 + // handle onesignal response. Start next batch if there's not an error. + let handleResponse = function(err) { + if (err) { + return promise.reject(err, tokens.slice(i, tokens.length())); + } + + if(offset => tokenlength) { + promise.resolve() + } else { + this.sendNext(); + } + }.bind(this) + + this.sendNext = function() { + post['include_android_reg_ids'] = tokens.slice(offset,offset+chunk); + offset+=chunk; + this.sendToOneSignal(post, handleResponse); + }.bind(this) + + this.sendNext() + + return promise; +} + +OneSignalPushAdapter.prototype.sendToGCM = function(data,tokens) { + let post = {}; + + if(data['alert']) { + post['contents'] = {en: data['alert']}; + delete data['alert']; + } + if(data['title']) { + post['title'] = {en: data['title']}; + delete data['title']; + } + if(data['uri']) { + post['url'] = data['uri']; + } + + post['data'] = data; + + let promise = new Parse.Promise(); + + var chunk = 2000 // OneSignal can process 2000 devices at a time + var tokenlength=tokens.length; + var offset = 0 + // handle onesignal response. Start next batch if there's not an error. + let handleResponse = function(err) { + if (err) { + return promise.reject(err, tokens.slice(i, tokens.length())); + } + + if(offset => tokenlength) { + promise.resolve() + } else { + this.sendNext(); + } + }.bind(this); + + this.sendNext = function() { + post['include_android_reg_ids'] = tokens.slice(offset,offset+chunk);; + offset+=chunk; + this.sendToOneSignal(post, handleResponse); + }.bind(this); + + this.sendNext(); + return promise; +} + + +OneSignalPushAdapter.prototype.sendToOneSignal = function(data, cb) { + let headers = { + "Content-Type": "application/json", + "Authorization": "Basic "+this.OneSignalConfig['apiKey'] + }; + let options = { + host: "onesignal.com", + port: 443, + path: "/api/v1/notifications", + method: "POST", + headers: headers + }; + data['app_id'] = this.OneSignalConfig['appId']; + + let request = this.https.request(options, function(res) { + cb(null); + }); + request.on('error', function(e) { + cb(e); + }); + console.log(data); + request.write(JSON.stringify(data)) + request.end(); +} +/**g + * Classify the device token of installations based on its device type. + * @param {Object} installations An array of installations + * @param {Array} validPushTypes An array of valid push types(string) + * @returns {Object} A map whose key is device type and value is an array of device + */ +function classifyInstallation(installations, validPushTypes) { + // Init deviceTokenMap, create a empty array for each valid pushType + let deviceMap = {}; + for (let validPushType of validPushTypes) { + deviceMap[validPushType] = []; + } + for (let installation of installations) { + // No deviceToken, ignore + if (!installation.deviceToken) { + continue; + } + let pushType = installation.deviceType; + if (deviceMap[pushType]) { + deviceMap[pushType].push({ + deviceToken: installation.deviceToken + }); + } else { + console.log('Unknown push type from installation %j', installation); + } + } + return deviceMap; +} + +if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') { + OneSignalPushAdapter.classifyInstallation = classifyInstallation; +} +module.exports = OneSignalPushAdapter; diff --git a/src/RestWrite.js b/src/RestWrite.js index 2a2b0ed2..b6c8f126 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -485,11 +485,6 @@ RestWrite.prototype.handleInstallation = function() { this.data.installationId = this.data.installationId.toLowerCase(); } - if (this.data.deviceToken && this.data.deviceType == 'android') { - throw new Parse.Error(114, - 'deviceToken may not be set for deviceType android'); - } - var promise = Promise.resolve(); if (this.query && this.query.objectId) {