├── config └── node_config.json ├── .gitignore ├── package.json ├── server.js ├── LICENSE ├── services ├── web-push.js ├── gcm.js ├── validator.js ├── apn.js └── push.js └── README.md /config/node_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "USE_KUE": false, 3 | "MAX_CONCURRENT_JOBS": 10 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "push-microservice", 3 | "version": "0.1.0", 4 | "description": "A simple node.js microservice for sending push notifications.", 5 | "keywords": [ 6 | "micro", 7 | "service", 8 | "microservice", 9 | "push", 10 | "notifications", 11 | "gcm", 12 | "apn", 13 | "web push" 14 | ], 15 | "main": "server.js", 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "author": "Mihail Cristian Dumitru", 20 | "license": "MIT", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/Xzya/PushMicroservice.git" 24 | }, 25 | "dependencies": { 26 | "seneca": "2.0.1", 27 | "web-push": "2.1.1", 28 | "apn": "1.7.5", 29 | "node-gcm": "0.14.0", 30 | "kue": "0.10.5" 31 | } 32 | } -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var seneca = require("seneca")(); 2 | var config = require("./config/node_config.json"); 3 | 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Configuration 7 | |-------------------------------------------------------------------------- 8 | */ 9 | if (config.USE_KUE) { 10 | var kue = require("kue"); 11 | 12 | // gracefully shut down kue 13 | process.once('SIGTERM', function (sig) { 14 | kue.createQueue().shutdown(5000, function (err) { 15 | console.log('Kue shutdown: ', err || ''); 16 | process.exit(0); 17 | }); 18 | }); 19 | 20 | kue.createQueue().watchStuckJobs(60 * 1000); 21 | 22 | // optional, start kue web interface 23 | kue.app.listen(3001); 24 | } 25 | 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | Start server 29 | |-------------------------------------------------------------------------- 30 | */ 31 | // start seneca 32 | seneca.use('./services/push', config); 33 | seneca.listen({ pin: "role:push" }); 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mihail Cristian Dumitru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /services/web-push.js: -------------------------------------------------------------------------------- 1 | module.exports = function (options) { 2 | var seneca = this; 3 | var plugin = "web-push"; 4 | 5 | /* 6 | |-------------------------------------------------------------------------- 7 | | Dependencies 8 | |-------------------------------------------------------------------------- 9 | */ 10 | var webPush = require("web-push"); 11 | var Validator = new (require("./validator"))(); 12 | 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Kue 16 | |-------------------------------------------------------------------------- 17 | */ 18 | if (options.USE_KUE) { 19 | var queue = require("kue").createQueue(); 20 | 21 | queue.process("firefox", options.MAX_CONCURRENT_JOBS, function (job, done) { 22 | var args = job.data.args; 23 | 24 | pushFirefox(args, function (err, result) { 25 | done(err, result); 26 | }); 27 | }); 28 | 29 | queue.process("chrome", options.MAX_CONCURRENT_JOBS, function (job, done) { 30 | var args = job.data.args; 31 | 32 | pushChrome(args, function (err, result) { 33 | done(err, result); 34 | }); 35 | }); 36 | } 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Firefox 44+ 41 | |-------------------------------------------------------------------------- 42 | */ 43 | function pushFirefox(args, callback) { 44 | Validator.validateFirefox(args, function (err) { 45 | if (err) return callback(err); 46 | 47 | webPush.sendNotification(args.token, args.params) 48 | .then(function (result) { 49 | callback(null, { result: result }); 50 | }).catch(function (err) { 51 | callback(err, null); 52 | }); 53 | }); 54 | } 55 | 56 | seneca.add({ role: plugin, cmd: "firefox" }, function (args, callback) { 57 | if (options.USE_KUE) { 58 | queue.create("firefox", { 59 | title: "Firefox", 60 | args: args 61 | }).attempts(10).removeOnComplete(false).save(); 62 | 63 | callback(null, { result: "Processing..." }) 64 | } else { 65 | pushFirefox(args, callback); 66 | } 67 | }); 68 | 69 | /* 70 | |-------------------------------------------------------------------------- 71 | | Chrome 50+ 72 | |-------------------------------------------------------------------------- 73 | */ 74 | function pushChrome(args, callback) { 75 | Validator.validateChrome(args, function (err) { 76 | if (err) return callback(err); 77 | 78 | webPush.setGCMAPIKey(args.config.gcm.apiKey); 79 | webPush.sendNotification(args.token, args.params) 80 | .then(function (result) { 81 | callback(null, { result: result }); 82 | }).catch(function (err) { 83 | callback(err, null); 84 | }); 85 | }); 86 | } 87 | 88 | seneca.add({ role: plugin, cmd: "chrome" }, function (args, callback) { 89 | if (options.USE_KUE) { 90 | queue.create("chrome", { 91 | title: "Chrome", 92 | args: args 93 | }).attempts(10).removeOnComplete(false).save(); 94 | 95 | callback(null, { result: "Processing..." }) 96 | } else { 97 | pushChrome(args, callback); 98 | } 99 | }); 100 | 101 | return { 102 | name: plugin 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /services/gcm.js: -------------------------------------------------------------------------------- 1 | module.exports = function (options) { 2 | var seneca = this; 3 | var plugin = "gcm"; 4 | 5 | /* 6 | |-------------------------------------------------------------------------- 7 | | Dependencies 8 | |-------------------------------------------------------------------------- 9 | */ 10 | var gcm = require("node-gcm"); 11 | var Validator = new (require("./validator"))(); 12 | 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Kue 16 | |-------------------------------------------------------------------------- 17 | */ 18 | if (options.USE_KUE) { 19 | var queue = require("kue").createQueue(); 20 | 21 | queue.process("chrome:multicast", options.MAX_CONCURRENT_JOBS, function (job, done) { 22 | var args = job.data.args; 23 | 24 | pushChromeMulticast(args, function (err, result) { 25 | done(err, result); 26 | }); 27 | }); 28 | 29 | queue.process("android", options.MAX_CONCURRENT_JOBS, function (job, done) { 30 | var args = job.data.args; 31 | 32 | pushAndroid(args, function (err, result) { 33 | done(err, result); 34 | }); 35 | }); 36 | } 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Chrome multicast 41 | |-------------------------------------------------------------------------- 42 | */ 43 | function pushChromeMulticast(args, callback) { 44 | Validator.validateChromeMulticast(args, function (err, data) { 45 | if (err) return callback(err, null); 46 | 47 | var message = new gcm.Message(); 48 | 49 | var sender = new gcm.Sender(args.config.gcm.apiKey); 50 | 51 | sender.sendNoRetry(message, { registrationTokens: args.tokens }, function (err, response) { 52 | if (err) callback(err, null); 53 | else callback(null, { result: response }); 54 | }); 55 | }); 56 | } 57 | 58 | seneca.add({ role: plugin, cmd: "chrome:multicast" }, function (args, callback) { 59 | if (options.USE_KUE) { 60 | queue.create("chrome:multicast", { 61 | title: "Chrome multicast", 62 | args: args 63 | }).attempts(10).removeOnComplete(false).save(); 64 | 65 | callback(null, { result: "Processing..." }) 66 | } else { 67 | pushChromeMulticast(args, callback); 68 | } 69 | }); 70 | 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | Android 74 | |-------------------------------------------------------------------------- 75 | */ 76 | function pushAndroid(args, callback) { 77 | Validator.validateAndroid(args, function (err, data) { 78 | if (err) return callback(err, null); 79 | 80 | var message = new gcm.Message(args.params); 81 | 82 | var sender = new gcm.Sender(args.config.gcm.apiKey); 83 | 84 | sender.sendNoRetry(message, { registrationTokens: args.tokens }, function (err, response) { 85 | if (err) callback(err, null); 86 | else callback(null, { result: response }); 87 | }); 88 | }); 89 | } 90 | 91 | seneca.add({ role: plugin, cmd: "android" }, function (args, callback) { 92 | if (options.USE_KUE) { 93 | queue.create("android", { 94 | title: "Android", 95 | args: args 96 | }).attempts(10).removeOnComplete(false).save(); 97 | 98 | callback(null, { result: "Processing..." }) 99 | } else { 100 | pushAndroid(args, callback); 101 | } 102 | }); 103 | 104 | return { 105 | name: plugin 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /services/validator.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | /* 3 | |-------------------------------------------------------------------------- 4 | | Push request 5 | |-------------------------------------------------------------------------- 6 | */ 7 | this.validatePushRequest = function (args, callback) { 8 | if (!args.config) return callback(new Error("Missing config parameter.")); 9 | if (!args.notifications) return callback(new Error("Missing notifications parameter.")); 10 | if (args.notifications.length === 0) return callback(new Error("Needs at least one notification.")); 11 | callback(); 12 | }; 13 | 14 | /* 15 | |-------------------------------------------------------------------------- 16 | | Chrome multicast 17 | |-------------------------------------------------------------------------- 18 | */ 19 | this.validateChromeMulticast = function (args, callback) { 20 | if (!args.config) callback(new Error("Missing config parameter.")); 21 | if (!args.config.gcm) callback(new Error("Missing gcm parameter.")); 22 | if (!args.config.gcm.apiKey) callback(new Error("Missing GCM API Key.")); 23 | if (!args.tokens) callback(new Error("Missing tokens.")); 24 | if (args.tokens.length === 0) callback(new Error("Needs at least one token.")); 25 | if (args.tokens.length > 1000) callback(new Error("Needs at most 1000 tokens.")); 26 | callback(); 27 | }; 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Chrome 50+ 32 | |-------------------------------------------------------------------------- 33 | */ 34 | this.validateChrome = function (args, callback) { 35 | if (!args.config) callback(new Error("Missing config parameter.")); 36 | if (!args.config.gcm) callback(new Error("Missing gcm parameter.")); 37 | if (!args.config.gcm.apiKey) callback(new Error("Missing GCM API Key.")); 38 | if (!args.token) callback(new Error("Missing token.")); 39 | callback(); 40 | }; 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Firefox 45 | |-------------------------------------------------------------------------- 46 | */ 47 | this.validateFirefox = function (args, callback) { 48 | if (!args.token) callback(new Error("Missing token.")); 49 | callback(); 50 | }; 51 | 52 | /* 53 | |-------------------------------------------------------------------------- 54 | | Android 55 | |-------------------------------------------------------------------------- 56 | */ 57 | this.validateAndroid = function (args, callback) { 58 | if (!args.config) callback(new Error("Missing config parameter.")); 59 | if (!args.config.gcm) callback(new Error("Missing gcm parameter.")); 60 | if (!args.config.gcm.apiKey) callback(new Error("Missing GCM API Key.")); 61 | if (!args.tokens) callback(new Error("Missing tokens.")); 62 | if (args.tokens.length === 0) callback(new Error("Needs at least one token.")); 63 | if (args.tokens.length > 1000) callback(new Error("Needs at most 1000 tokens.")); 64 | callback(); 65 | }; 66 | 67 | /* 68 | |-------------------------------------------------------------------------- 69 | | iOS 70 | |-------------------------------------------------------------------------- 71 | */ 72 | this.validateiOS = function (args, callback) { 73 | if (!args.config) callback(new Error("Missing config parameter.")); 74 | if (!args.config.apn) callback(new Error("Missing apn parameter.")); 75 | if (!args.config.apn.ios) callback(new Error("Missing ios parameter.")); 76 | if (!args.config.apn.ios.connection) callback(new Error("Missing connection parameter.")); 77 | if (!args.config.apn.ios.connection.production) callback(new Error("Missing production parameter.")); 78 | if (!args.config.apn.ios.connection.cert) callback(new Error("Missing cert parameter.")); 79 | if (!args.config.apn.ios.connection.key) callback(new Error("Missing key parameter.")); 80 | if (!args.config.apn.ios.connection.passphrase) callback(new Error("Missing passphrase parameter.")); 81 | if (!args.tokens) callback(new Error("Missing tokens.")); 82 | if (args.tokens.length === 0) callback(new Error("Needs at least one token.")); 83 | callback(); 84 | }; 85 | 86 | /* 87 | |-------------------------------------------------------------------------- 88 | | Safari 89 | |-------------------------------------------------------------------------- 90 | */ 91 | this.validateSafari = function (args, callback) { 92 | if (!args.config) callback(new Error("Missing config parameter.")); 93 | if (!args.config.apn) callback(new Error("Missing apn parameter.")); 94 | if (!args.config.apn.safari) callback(new Error("Missing safari parameter.")); 95 | if (!args.config.apn.safari.connection) callback(new Error("Missing connection parameter.")); 96 | if (!args.config.apn.safari.connection.cert) callback(new Error("Missing cert parameter.")); 97 | if (!args.config.apn.safari.connection.key) callback(new Error("Missing key parameter.")); 98 | if (!args.config.apn.safari.connection.passphrase) callback(new Error("Missing passphrase parameter.")); 99 | if (!args.tokens) callback(new Error("Missing tokens.")); 100 | if (args.tokens.length === 0) callback(new Error("Needs at least one token.")); 101 | callback(); 102 | }; 103 | } 104 | -------------------------------------------------------------------------------- /services/apn.js: -------------------------------------------------------------------------------- 1 | module.exports = function (options) { 2 | var seneca = this; 3 | var plugin = "apn"; 4 | 5 | /* 6 | |-------------------------------------------------------------------------- 7 | | Dependencies 8 | |-------------------------------------------------------------------------- 9 | */ 10 | var apn = require("apn"); 11 | var Validator = new (require("./validator"))(); 12 | 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Kue 16 | |-------------------------------------------------------------------------- 17 | */ 18 | if (options.USE_KUE) { 19 | var queue = require("kue").createQueue(); 20 | 21 | queue.process("ios", options.MAX_CONCURRENT_JOBS, function (job, done) { 22 | var args = job.data.args; 23 | 24 | pushiOS(args, function (err, result) { 25 | done(err, result); 26 | }); 27 | }); 28 | 29 | queue.process("safari", options.MAX_CONCURRENT_JOBS, function (job, done) { 30 | var args = job.data.args; 31 | 32 | pushSafari(args, function (err, result) { 33 | done(err, result); 34 | }); 35 | }); 36 | } 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | iOS 41 | |-------------------------------------------------------------------------- 42 | */ 43 | function pushiOS(args, callback) { 44 | Validator.validateiOS(args, function (err, data) { 45 | if (err) return callback(err, null); 46 | 47 | var tokens = args.tokens; 48 | var notification = args.params; 49 | 50 | var apnOptions = { 51 | "cert": args.config.apn.ios.connection.cert, 52 | "key": args.config.apn.ios.connection.key, 53 | "passphrase": args.config.apn.ios.connection.passphrase, 54 | "production": args.config.apn.ios.connection.production, 55 | } 56 | var apnConnection = new apn.Connection(apnOptions); 57 | 58 | var notif = new apn.Notification(); 59 | notif.expiry = notification.expiry || notif.expiry; 60 | notif.badge = notification.badge || notif.badge; 61 | notif.sound = notification.sound || notif.sound; 62 | notif.alert = notification.alert || notif.alert; 63 | notif.payload = notification.payload || notif.payload; 64 | 65 | var devices = []; 66 | for (i in tokens) { 67 | var device = new apn.Device(tokens[i]); 68 | devices.push(device); 69 | } 70 | 71 | apnConnection.pushNotification(notif, devices); 72 | apnConnection.on("error", function (err) { 73 | console.log(err); 74 | }) 75 | callback(); 76 | }); 77 | } 78 | 79 | seneca.add({ role: plugin, cmd: "ios" }, function (args, callback) { 80 | if (options.USE_KUE) { 81 | queue.create("ios", { 82 | title: "iOS", 83 | args: args 84 | }).attempts(10).removeOnComplete(false).save(); 85 | 86 | callback(null, { result: "Processing..." }) 87 | } else { 88 | pushiOS(args, callback); 89 | } 90 | }); 91 | 92 | /* 93 | |-------------------------------------------------------------------------- 94 | | Safari 95 | |-------------------------------------------------------------------------- 96 | */ 97 | function pushSafari(args, callback) { 98 | Validator.validateiOS(args, function (err, data) { 99 | if (err) return callback(err, null); 100 | 101 | var tokens = args.tokens; 102 | var notification = args.params; 103 | 104 | var apnOptions = { 105 | "cert": args.config.apn.ios.connection.cert, 106 | "key": args.config.apn.ios.connection.key, 107 | "passphrase": args.config.apn.ios.connection.passphrase, 108 | "production": true, 109 | } 110 | var apnConnection = new apn.Connection(apnOptions); 111 | 112 | var notif = new apn.Notification(); 113 | notif.expiry = notification.expiry || notif.expiry; 114 | notif.badge = notification.badge || notif.badge; 115 | notif.sound = notification.sound || notif.sound; 116 | notif.alert = notification.alert || notif.alert; 117 | notif.payload = notification.payload || notif.payload; 118 | notif.urlArgs = notification.urlArgs || []; 119 | 120 | var devices = []; 121 | for (i in tokens) { 122 | var device = new apn.Device(tokens[i]); 123 | devices.push(device); 124 | } 125 | 126 | apnConnection.pushNotification(notif, devices); 127 | apnConnection.on("error", function (err) { 128 | console.log(err); 129 | }) 130 | callback(); 131 | }); 132 | } 133 | 134 | seneca.add({ role: plugin, cmd: "safari" }, function (args, callback) { 135 | if (options.USE_KUE) { 136 | queue.create("safari", { 137 | title: "Safari", 138 | args: args 139 | }).attempts(10).removeOnComplete(false).save(); 140 | 141 | callback(null, { result: "Processing..." }) 142 | } else { 143 | pushSafari(args, callback); 144 | } 145 | }); 146 | 147 | return { 148 | name: plugin 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /services/push.js: -------------------------------------------------------------------------------- 1 | module.exports = function (options) { 2 | var seneca = this; 3 | var plugin = "push"; 4 | 5 | /* 6 | |-------------------------------------------------------------------------- 7 | | Dependencies 8 | |-------------------------------------------------------------------------- 9 | */ 10 | var apn = require("apn"); 11 | var Validator = new (require("./validator"))(); 12 | 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Plugins 16 | |-------------------------------------------------------------------------- 17 | */ 18 | seneca.use("./services/web-push", options); 19 | seneca.use("./services/gcm", options); 20 | seneca.use("./services/apn", options); 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Kue 25 | |-------------------------------------------------------------------------- 26 | */ 27 | if (options.USE_KUE) { 28 | var queue = require("kue").createQueue(); 29 | 30 | queue.process("push", options.MAX_CONCURRENT_JOBS, function (job, done) { 31 | var args = job.data.args; 32 | 33 | pushNotification(args, function (err, result) { 34 | done(err, result); 35 | }); 36 | }); 37 | } 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Push 42 | |-------------------------------------------------------------------------- 43 | */ 44 | function pushNotification(args, callback) { 45 | Validator.validatePushRequest(args, function (err) { 46 | if (err) return callback(err); 47 | 48 | var config = args.config; 49 | var notifications = args.notifications; 50 | 51 | var chromeTokens = []; 52 | var chromeMulticastTokens = []; 53 | 54 | for (var i = 0; i < notifications.length; i++) { 55 | var notification = notifications[i]; 56 | var tokens = notification.tokens; 57 | var platform = notification.platform; 58 | 59 | // Chrome 50+, we can send the payload directly 60 | if (platform.type === "chrome" && parseInt(platform.version) >= 50) { 61 | 62 | // format the endpoints 63 | // add the gcm url if we only have the tokens 64 | tokens = tokens.map(function (value) { 65 | return "https://android.googleapis.com/gcm/send/" + value.split("/").pop(); 66 | }); 67 | 68 | for (var j = 0; j < tokens.length; j++) { 69 | seneca.act({ role: "web-push", cmd: "chrome", token: tokens[j], config: config, params: notification.web }, function (err, result) { 70 | 71 | }); 72 | } 73 | } 74 | // Chrome < 50, we can't send payload, so send a multicast notification 75 | else if (platform.type === "chrome" && parseInt(platform.version) < 50) { 76 | 77 | var batches = []; 78 | 79 | // if we have more than 1000 tokens, split them in 1000 groups 80 | while (tokens.length > 0) { 81 | batches.push(tokens.splice(0, 1000)); 82 | } 83 | 84 | for (var j = 0; j < batches.length; j++) { 85 | seneca.act({ role: "gcm", cmd: "chrome:multicast", config: config, tokens: batches[j] }, function (err, result) { 86 | 87 | }); 88 | } 89 | } 90 | // Firefox 44+, we can send the payload directly 91 | else if (platform.type === "firefox" && parseInt(platform.version) >= 44) { 92 | 93 | // format the endpoints 94 | // add the mozilla url if we only have the tokens 95 | tokens = tokens.map(function (value) { 96 | return "https://updates.push.services.mozilla.com/push/v1/" + value.split("/").pop(); 97 | }); 98 | 99 | for (var j = 0; j < tokens.length; j++) { 100 | seneca.act({ role: "web-push", cmd: "firefox", token: tokens[j], params: notification.web }, function (err, result) { 101 | 102 | }); 103 | } 104 | } 105 | // Android, we can send the payload directly 106 | else if (platform.type === "android") { 107 | 108 | var batches = []; 109 | 110 | // if we have more than 1000 tokens, split them in 1000 groups 111 | while (tokens.length > 0) { 112 | batches.push(tokens.splice(0, 1000)); 113 | } 114 | 115 | for (var j = 0; j < batches.length; j++) { 116 | seneca.act({ role: "gcm", cmd: "android", config: config, tokens: batches[j], params: notification.android }, function (err, result) { 117 | 118 | }); 119 | } 120 | } 121 | // iOS, we can send the payload directly 122 | else if (platform.type === "ios") { 123 | 124 | seneca.act({ role: "apn", cmd: "ios", config: config, tokens: tokens, params: notification.ios }, function (err, result) { 125 | 126 | }); 127 | } 128 | // Safari, we can send the payload directly 129 | else if (platform.type === "safari") { 130 | 131 | seneca.act({ role: "apn", cmd: "safari", config: config, tokens: tokens, params: notification.safari }, function (err, result) { 132 | 133 | }); 134 | } 135 | } 136 | callback(); 137 | }); 138 | } 139 | 140 | seneca.add({ role: plugin, cmd: "push" }, function (args, callback) { 141 | if (options.USE_KUE) { 142 | queue.create("push", { 143 | title: "Push notification", 144 | args: args 145 | }).attempts(10).removeOnComplete(true).save(); 146 | 147 | callback(null, { result: "Processing..." }) 148 | } else { 149 | pushNotification(args, callback); 150 | } 151 | }); 152 | 153 | return { 154 | name: plugin 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Push Microservice 2 | 3 | This is a simple node.js microservice for sending push notifications. 4 | 5 | # Supported platforms 6 | - Chrome 42+ with no payload (Desktop and Android) (multicast support) 7 | - Chrome 50+ with payload (Desktop) 8 | - Firefox 44+ with payload (Desktop) 9 | - Safari (Desktop) 10 | - Android 11 | - iOS 12 | 13 | # Technologies used 14 | - [Seneca][seneca] for integrating the microservice easily with other services 15 | - [node-apn][node-apn] for Safari and iOS notifications 16 | - [node-gcm][node-gcm] for Android and Chrome multicast notifications 17 | - [web-push][web-push] for Chrome 50+ and Firefox 44+ notifications with payload 18 | - [kue][kue] for queueing tasks (disabled by default, you need to set USE_KUE to true in config/node_config.json, it uses Redis for storing the tasks) 19 | 20 | # Usage 21 | The request object is composed of two objects: 22 | ```javascript 23 | { 24 | "notifications": [...], 25 | "config": {...} 26 | } 27 | ``` 28 | 29 | The config object contains information necessary to send the notifications: 30 | - gcm 31 | - apiKey: the GCM API Key required to send Android and Chrome notifications 32 | - apn 33 | - ios 34 | - connection 35 | - cert: path to the ios certificate (pem) 36 | - key: path to the ios key (pem) 37 | - passphrase: certificate password, can be empty, but not null 38 | - production: whether the certificate is for development or production environments 39 | - safari 40 | - connection 41 | - cert: path to the safari certificate (pem) 42 | - key: path to the safari key (pem) 43 | - passphrase: certificate password, can be empty, but not null 44 | The notifications parameter is an array of notifications. A notification can take the following form: 45 | - platform 46 | - type: the subscriber's platform. Can be chrome/android/firefox/ios/safari 47 | - version: the version of the platform. E.g. 50 for Chrome 50 48 | - mobile: Whether the platform is mobile. E.g. for Chrome on Android this should be true 49 | - tokens: an array of tokens. For Chrome 50+ and Firefox 44+ with payload, this should only contain one token per notification 50 | - web: Only required for Chrome 50+ and Firefox 44+ 51 | - userPublicKey: subscriber's public key 52 | - userAuth: subscriber's auth key 53 | - payload: payload information. Must be a string (you can use JSON.stringify()) 54 | - ios: Only required for iOS 55 | - safari: Only required for safari 56 | - android: Only required for android 57 | 58 | Some comments: 59 | - For Chrome 50+ and Firefox 44+, if you are sending a payload, you must specify the userPublicKey and userAuth. Also you must only specify one token per notification (it's still an array, but it only contains one token "tokens": ["TOKEN"]) 60 | - For Chrome <50 the payload is not supported. You can specity however many tokens you want, they will be batched into 1000's. 61 | 62 | For more information about the iOS payload, please refer to [Apple's Documentation][apple-ios-doc]. 63 | 64 | For more information about the Android payload, please refer to [Google's Documentation][google-android-doc]. 65 | 66 | For more information about the Safari payload, please refer to [Apple's Safari Documentation][apple-safari-doc]. 67 | 68 | For more information about the Firefox payload, please refer to [Mozilla's Documentation][mozilla-doc]. 69 | 70 | Also please refer to the node modules mentioned above, they contain more information. 71 | 72 | # Sample request: 73 | ```javascript 74 | var requestParams = { 75 | "notifications": [ 76 | { 77 | "platform": { 78 | "type": "chrome", 79 | "version": "50", 80 | "mobile": false 81 | }, 82 | "tokens": [ 83 | "SUBSCRIPTION_TOKEN_HERE" 84 | ], 85 | "web": { 86 | "userPublicKey": "SUBSCRIPTION_PUBLIC_KEY_HERE", 87 | "userAuth": "SUBSCRIPTION_AUTH_HERE", 88 | "payload": JSON.stringify({ 89 | "notification": { 90 | "title": "Notification title", 91 | "content": "Notification content" 92 | } 93 | }) 94 | } 95 | } 96 | ], 97 | "config": { 98 | "gcm": { 99 | "apiKey": "YOUR_API_KEY_HERE" 100 | }, 101 | "apn": { 102 | "safari": { 103 | "connection": { 104 | "cert": "/path/to/cert.pem", 105 | "key": "/path/to/key.pem", 106 | "passphrase": "YOUR_PASSPHRASE_HERE" 107 | } 108 | }, 109 | "ios": { 110 | "connection": { 111 | "production": true, 112 | "cert": "/path/to/cert.pem", 113 | "key": "/path/to/key.pem", 114 | "passphrase": "YOUR_PASSPHRASE_HERE" 115 | } 116 | } 117 | } 118 | } 119 | } 120 | 121 | seneca.act({ role: "push", cmd: "push", notifications: requestParams.notifications, config: requestParams.config }, function (err, result) { 122 | console.log(err); 123 | console.log(result); 124 | }); 125 | ``` 126 | 127 | # Licence 128 | 129 | The MIT License (MIT) 130 | 131 | Copyright (c) 2016 Mihail Cristian Dumitru 132 | 133 | Permission is hereby granted, free of charge, to any person obtaining a copy 134 | of this software and associated documentation files (the "Software"), to deal 135 | in the Software without restriction, including without limitation the rights 136 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 137 | copies of the Software, and to permit persons to whom the Software is 138 | furnished to do so, subject to the following conditions: 139 | 140 | The above copyright notice and this permission notice shall be included in all 141 | copies or substantial portions of the Software. 142 | 143 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 144 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 145 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 146 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 147 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 148 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 149 | SOFTWARE. 150 | 151 | [seneca]: 152 | [node-apn]: 153 | [node-gcm]: 154 | [web-push]: 155 | [apple-ios-doc]: 156 | [apple-safari-doc]: 157 | [google-android-doc]: 158 | [mozilla-doc]: 159 | [kue]: 160 | --------------------------------------------------------------------------------