├── screenshot1.PNG ├── apn ├── icons │ └── apple_icon.png ├── apn-app.js ├── apn-token.js ├── apn-app.html ├── apn-token.html ├── apn.html ├── apn.js ├── apn-notification.js └── apn-notification.html ├── gcm ├── icons │ └── android_icon.png ├── gcm-configuration.js ├── gcm-configuration.html ├── gcm.html ├── gcm.js ├── gcm-notification.js └── gcm-notification.html ├── web ├── icons │ └── chrome_icon.png ├── web.html ├── web-notification.js ├── web.js └── web-notification.html ├── .gitignore ├── package.json ├── README.md └── exampleFlow.json /screenshot1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xzya/node-red-contrib-push/master/screenshot1.PNG -------------------------------------------------------------------------------- /apn/icons/apple_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xzya/node-red-contrib-push/master/apn/icons/apple_icon.png -------------------------------------------------------------------------------- /gcm/icons/android_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xzya/node-red-contrib-push/master/gcm/icons/android_icon.png -------------------------------------------------------------------------------- /web/icons/chrome_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xzya/node-red-contrib-push/master/web/icons/chrome_icon.png -------------------------------------------------------------------------------- /gcm/gcm-configuration.js: -------------------------------------------------------------------------------- 1 | module.exports = function (RED) { 2 | function GCMConfigurationNode(config) { 3 | RED.nodes.createNode(this, config); 4 | this.apiKey = config.apiKey; 5 | } 6 | RED.nodes.registerType("gcm-configuration", GCMConfigurationNode); 7 | } -------------------------------------------------------------------------------- /apn/apn-app.js: -------------------------------------------------------------------------------- 1 | module.exports = function (RED) { 2 | function APNAppNode(n) { 3 | RED.nodes.createNode(this, n); 4 | this.token = RED.nodes.getNode(n.token); 5 | this.topic = n.topic; 6 | this.production = n.production; 7 | } 8 | RED.nodes.registerType("apn-app", APNAppNode); 9 | } -------------------------------------------------------------------------------- /apn/apn-token.js: -------------------------------------------------------------------------------- 1 | module.exports = function (RED) { 2 | function APNTokenNode(config) { 3 | RED.nodes.createNode(this, config); 4 | this.key = config.key; 5 | this.keyId = config.keyId; 6 | this.teamId = config.teamId; 7 | } 8 | RED.nodes.registerType("apn-token", APNTokenNode); 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | lib-cov 3 | *.seed 4 | *.log 5 | *.csv 6 | *.dat 7 | *.out 8 | *.pid 9 | *.gz 10 | pids 11 | logs 12 | results 13 | node_modules 14 | npm-debug.log 15 | mochahelper.js 16 | .idea/ 17 | .settings/ 18 | dist 19 | .tmp 20 | .sass-cache 21 | bower_components 22 | options.mine.js 23 | db/ 24 | data/ 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-contrib-push", 3 | "version": "0.0.1", 4 | "description": "Push notifications for node-red", 5 | "author": "Mihail Cristian Dumitru", 6 | "keywords": [ 7 | "node-red", 8 | "apn", 9 | "gcm", 10 | "web", 11 | "push", 12 | "notifications" 13 | ], 14 | "node-red": { 15 | "nodes": { 16 | "apn": "apn/apn.js", 17 | "apn-token": "apn/apn-token.js", 18 | "apn-app": "apn/apn-app.js", 19 | "apn-notification": "apn/apn-notification.js", 20 | "gcm": "gcm/gcm.js", 21 | "gcm-configuration": "gcm/gcm-configuration.js", 22 | "gcm-notification": "gcm/gcm-notification.js", 23 | "web": "web/web.js", 24 | "web-notification": "web/web-notification.js" 25 | } 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/Xzya/node-red-contrib-push" 30 | }, 31 | "dependencies": { 32 | "apn": "2.1.3", 33 | "node-gcm": "0.14.4", 34 | "web-push": "3.2.2" 35 | }, 36 | "license": "Apache-2.0" 37 | } -------------------------------------------------------------------------------- /gcm/gcm-configuration.html: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | 26 | -------------------------------------------------------------------------------- /apn/apn-app.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 35 | 36 | -------------------------------------------------------------------------------- /gcm/gcm.html: -------------------------------------------------------------------------------- 1 | 18 | 19 | 29 | 30 | -------------------------------------------------------------------------------- /gcm/gcm.js: -------------------------------------------------------------------------------- 1 | module.exports = function (RED) { 2 | function GCMNode(n) { 3 | RED.nodes.createNode(this, n); 4 | var node = this; 5 | 6 | // Retrieve the config node 7 | this.gcmConfiguration = RED.nodes.getNode(n.gcmConfiguration); 8 | 9 | this.on('input', function (msg) { 10 | 11 | node.status({ fill: "blue", shape: "dot", text: " " }) 12 | 13 | var content = msg.notification; 14 | 15 | var gcm = require('node-gcm'); 16 | 17 | var message = new gcm.Message(content); 18 | var sender = new gcm.Sender(this.gcmConfiguration.apiKey); 19 | 20 | var recipient; 21 | 22 | if (msg.to) { 23 | recipient = { to: msg.to } 24 | } else if (msg.topic) { 25 | recipient = { topic: msg.topic }; 26 | } else if (msg.condition) { 27 | recipient = { condition: msg.condition } 28 | } else if (msg.tokens && msg.tokens.length > 0) { 29 | recipient = { registrationTokens: msg.tokens }; 30 | } else if (msg.registrationTokens && msg.registrationTokens.length > 0) { 31 | recipient = { registrationTokens: msg.registrationTokens }; 32 | } else { 33 | var err = new Error("Missing recipient"); 34 | node.status({ fill: "red", shape: "dot", text: err.message }) 35 | node.error(err); 36 | return; 37 | } 38 | 39 | sender.sendNoRetry(message, recipient, function (err, response) { 40 | if (err) { 41 | node.status({ fill: "red", shape: "dot", text: err.message }) 42 | node.error(err); 43 | return; 44 | } 45 | 46 | node.status({ 47 | fill: "green", 48 | shape: "dot", 49 | text: response.success + " sent, " + response.failure + " failed." 50 | }) 51 | msg.result = response; 52 | node.send(msg) 53 | }); 54 | 55 | }); 56 | 57 | this.on('close', function () { 58 | node.status({}); 59 | }); 60 | } 61 | RED.nodes.registerType("gcm", GCMNode); 62 | } -------------------------------------------------------------------------------- /web/web.html: -------------------------------------------------------------------------------- 1 | 18 | 19 | 29 | 30 | -------------------------------------------------------------------------------- /apn/apn-token.html: -------------------------------------------------------------------------------- 1 | 43 | 44 | 65 | 66 | -------------------------------------------------------------------------------- /apn/apn.html: -------------------------------------------------------------------------------- 1 | 18 | 19 | 29 | 30 | -------------------------------------------------------------------------------- /web/web-notification.js: -------------------------------------------------------------------------------- 1 | module.exports = function (RED) { 2 | function WebNotificationNode(n) { 3 | RED.nodes.createNode(this, n); 4 | 5 | this.content = n; 6 | 7 | var node = this; 8 | 9 | this.on('input', function (msg) { 10 | var payload = {}; 11 | 12 | node.status({}); 13 | 14 | try { 15 | 16 | if (node.content.title) { 17 | if (!payload.notification) { 18 | payload.notification = {}; 19 | } 20 | payload.notification.title = node.content.title; 21 | } 22 | 23 | if (node.content.body) { 24 | if (!payload.notification) { 25 | payload.notification = {}; 26 | } 27 | payload.notification.body = node.content.body; 28 | } 29 | 30 | if (node.content.sound) { 31 | if (!payload.notification) { 32 | payload.notification = {}; 33 | } 34 | payload.notification.sound = node.content.sound; 35 | } 36 | 37 | if (node.content.payload) { 38 | var data = {}; 39 | var keyValArray = JSON.parse(node.content.payload) 40 | for (var i = 0; i < keyValArray.length; i++) { 41 | switch (keyValArray[i].type) { 42 | case "str": 43 | var value = String(keyValArray[i].value) 44 | data[keyValArray[i].key] = value 45 | break 46 | case "num": 47 | var value = parseFloat(keyValArray[i].value) 48 | if (!Number.isNaN(value)) { 49 | data[keyValArray[i].key] = value 50 | } else { 51 | throw new Error("Could not parse " + keyValArray[i].value + " into a number.") 52 | } 53 | break 54 | case "bool": 55 | var value = keyValArray[i].value == true 56 | data[keyValArray[i].key] = value 57 | break 58 | case "json": 59 | var value = JSON.parse(keyValArray[i].value) 60 | data[keyValArray[i].key] = value 61 | break 62 | } 63 | } 64 | payload.data = data; 65 | } 66 | 67 | msg.notification = payload; 68 | 69 | node.send(msg); 70 | 71 | } catch (err) { 72 | node.status({ fill: "red", shape: "dot", text: err.message }) 73 | } 74 | }); 75 | 76 | this.on('close', function () { 77 | node.status({}); 78 | }); 79 | } 80 | RED.nodes.registerType("web-notification", WebNotificationNode); 81 | } -------------------------------------------------------------------------------- /web/web.js: -------------------------------------------------------------------------------- 1 | module.exports = function (RED) { 2 | function WebNode(n) { 3 | RED.nodes.createNode(this, n); 4 | var node = this; 5 | 6 | // Retrieve the config node 7 | this.gcmConfiguration = RED.nodes.getNode(n.gcmConfiguration); 8 | 9 | this.on('input', function (msg) { 10 | 11 | try { 12 | node.status({ fill: "blue", shape: "dot", text: " " }) 13 | 14 | var payload; 15 | if ((msg.notification != null) && (typeof msg.notification === "object")) { 16 | payload = JSON.stringify(msg.notification) 17 | } 18 | 19 | var options; 20 | if (node.gcmConfiguration && node.gcmConfiguration.apiKey) { 21 | options = { 22 | gcmAPIKey: node.gcmConfiguration.apiKey 23 | } 24 | } 25 | 26 | if (msg.tokens && msg.tokens.length > 0) { 27 | var webPush = require('web-push'); 28 | 29 | function sendNotification(subscription, payload, options) { 30 | return new Promise(function (fulfill, reject) { 31 | webPush.sendNotification(subscription, payload, options).then(function (response) { 32 | fulfill({ 33 | sent: { 34 | endpoint: subscription.endpoint 35 | } 36 | }); 37 | }).catch(function (err) { 38 | fulfill({ 39 | failed: JSON.parse(JSON.stringify(err)) // for some reason it only puts the message without this 40 | }) 41 | }) 42 | }) 43 | } 44 | 45 | var calls = [] 46 | 47 | for (var i = 0; i < msg.tokens.length; i++) { 48 | calls.push(sendNotification(msg.tokens[i], payload, options)) 49 | } 50 | 51 | Promise.all(calls).then(function (results) { 52 | msg.result = { 53 | sent: results.reduce((prev, current) => { 54 | if (current.sent) { 55 | prev.push(current.sent) 56 | } 57 | return prev 58 | }, []), 59 | failed: results.reduce((prev, current) => { 60 | if (current.failed) { 61 | prev.push(current.failed) 62 | } 63 | return prev 64 | }, []) 65 | } 66 | node.status({ 67 | fill: "green", 68 | shape: "dot", 69 | text: msg.result.sent.length + " sent, " + msg.result.failed.length + " failed." 70 | }) 71 | node.send(msg) 72 | }) 73 | 74 | } else { 75 | throw new Error("Missing recipient"); 76 | } 77 | 78 | } catch (err) { 79 | node.status({ fill: "red", shape: "dot", text: err.message }) 80 | node.error(err); 81 | return; 82 | } 83 | }); 84 | 85 | this.on('close', function () { 86 | node.status({}); 87 | }); 88 | } 89 | RED.nodes.registerType("web", WebNode); 90 | } -------------------------------------------------------------------------------- /apn/apn.js: -------------------------------------------------------------------------------- 1 | module.exports = function (RED) { 2 | function APNNode(n) { 3 | RED.nodes.createNode(this, n); 4 | var node = this; 5 | 6 | // Retrieve the app node 7 | this.app = RED.nodes.getNode(n.app); 8 | 9 | this.on('input', function (msg) { 10 | 11 | node.status({ fill: "blue", shape: "dot", text: " " }) 12 | 13 | var rawNotification = msg.rawNotification; 14 | var content = msg.notification; 15 | 16 | if (rawNotification || content) { 17 | var apn = require("apn"); 18 | var provider = new apn.Provider({ 19 | token: node.app.token, 20 | production: node.app.production 21 | }) 22 | 23 | var notification = new apn.Notification(); 24 | notification.topic = node.app.topic; 25 | 26 | if (rawNotification) { 27 | notification.rawPayload = rawNotification; 28 | } else { 29 | if (content.title && content.body) { 30 | notification.title = content.title; 31 | notification.body = content.body; 32 | } else if (content.body) { 33 | notification.alert = content.body; 34 | } 35 | if (content.sound) { 36 | notification.sound = content.sound; 37 | } 38 | if (content.badge != null) { 39 | notification.badge = content.badge; 40 | } 41 | if (content.action) { 42 | notification.action = content.action; 43 | } 44 | if (content.category) { 45 | notification.category = content.category; 46 | } 47 | if (content.contentAvailable) { 48 | notification.contentAvailable = 1; 49 | } 50 | if (content.mutableContent) { 51 | notification.mutableContent = 1; 52 | } 53 | if (content.expiry) { 54 | notification.expiry = content.expiry; 55 | } 56 | if (content.urlArgs) { 57 | notification.urlArgs = content.urlArgs; 58 | } 59 | if (content.priority && (content.alert || content.title || content.body || content.sound || content.badge)) { 60 | notification.priority = content.priority; 61 | } 62 | if (content.payload) { 63 | notification.payload = content.payload; 64 | } 65 | } 66 | 67 | var tokens; 68 | 69 | if (msg.tokens && msg.tokens.length > 0) { 70 | tokens = msg.tokens; 71 | } else { 72 | var err = new Error("Missing recipient"); 73 | node.status({ fill: "red", shape: "dot", text: err.message }) 74 | node.error(err); 75 | return; 76 | } 77 | 78 | provider.send(notification, tokens).then(function (response) { 79 | node.status({ 80 | fill: "green", 81 | shape: "dot", 82 | text: response.sent.length + " sent, " + response.failed.length + " failed." 83 | }) 84 | msg.result = response; 85 | node.send(msg) 86 | }).catch(function (err) { 87 | node.status({ fill: "red", shape: "dot", text: err.message }) 88 | node.error(err); 89 | }) 90 | } 91 | }); 92 | 93 | this.on('close', function () { 94 | node.status({}); 95 | }); 96 | 97 | } 98 | RED.nodes.registerType("apn", APNNode); 99 | } -------------------------------------------------------------------------------- /gcm/gcm-notification.js: -------------------------------------------------------------------------------- 1 | module.exports = function (RED) { 2 | function GCMNotificationNode(n) { 3 | RED.nodes.createNode(this, n); 4 | 5 | this.content = n; 6 | 7 | var node = this; 8 | 9 | this.on('input', function (msg) { 10 | var payload = {}; 11 | 12 | node.status({}); 13 | 14 | try { 15 | 16 | if (node.content.title) { 17 | if (!payload.notification) { 18 | payload.notification = {}; 19 | } 20 | payload.notification.title = node.content.title; 21 | } 22 | 23 | if (node.content.body) { 24 | if (!payload.notification) { 25 | payload.notification = {}; 26 | } 27 | payload.notification.body = node.content.body; 28 | } 29 | 30 | if (node.content.sound) { 31 | payload.sound = node.content.sound; 32 | } 33 | 34 | if (node.content.payload) { 35 | var data = {}; 36 | var keyValArray = JSON.parse(node.content.payload) 37 | for (var i = 0; i < keyValArray.length; i++) { 38 | switch (keyValArray[i].type) { 39 | case "str": 40 | var value = String(keyValArray[i].value) 41 | data[keyValArray[i].key] = value 42 | break 43 | case "num": 44 | var value = parseFloat(keyValArray[i].value) 45 | if (!Number.isNaN(value)) { 46 | data[keyValArray[i].key] = value 47 | } else { 48 | throw new Error("Could not parse " + keyValArray[i].value + " into a number.") 49 | } 50 | break 51 | case "bool": 52 | var value = keyValArray[i].value == true 53 | data[keyValArray[i].key] = value 54 | break 55 | case "json": 56 | var value = JSON.parse(keyValArray[i].value) 57 | data[keyValArray[i].key] = value 58 | break 59 | } 60 | } 61 | payload.data = data; 62 | } 63 | 64 | if (node.content.expiry) { 65 | var units = node.content.expiry.split(" ").map(function (unit) { 66 | return Number(unit) 67 | }) 68 | var minutes = units[0] || 0; 69 | var hours = units[1] || 0; 70 | var days = units[2] || 0; 71 | var weeks = units[3] || 0; 72 | 73 | payload.timeToLive = (60 * minutes) 74 | + (60 * 60 * hours) 75 | + (60 * 60 * 24 * days) 76 | + (60 * 60 * 24 * 7 * weeks) 77 | } 78 | 79 | if (node.content.priority) { 80 | payload.priority = node.content.priority; 81 | } 82 | 83 | if (node.content.dryRun) { 84 | payload.dryRun = node.content.dryRun; 85 | } 86 | 87 | msg.notification = payload; 88 | 89 | node.send(msg); 90 | 91 | } catch (err) { 92 | node.status({ fill: "red", shape: "dot", text: err.message }) 93 | } 94 | }); 95 | 96 | this.on('close', function () { 97 | node.status({}); 98 | }); 99 | } 100 | RED.nodes.registerType("gcm-notification", GCMNotificationNode); 101 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Node-RED Push Notification nodes 2 | ==================================== 3 | 4 | 5 | `node-red-contrib-push` is a [Node-RED](http://nodered.org/docs/creating-nodes/packaging.html) package that allows you to send APN, GCM and Web push notifications. 6 | 7 | It uses the [node-apn](https://github.com/node-apn/node-apn) library for APN notifications, [node-gcm](https://github.com/ToothlessGear/node-gcm) for GCM notifications and [web-push](https://github.com/web-push-libs/web-push) for Web notifications. 8 | 9 | ## Table of Contents 10 | - [APN](#apn) 11 | - [GCM](#gcm) 12 | - [Web](#web) 13 | - [Additional information](#additional) 14 | - [Example flow](#example) 15 | - [Screenshots](#screenshots) 16 | - [License](#license) 17 | 18 | ## APN 19 | 20 | You can use the ```apn``` node to send notifications to iOS and Safari devices. You will need to configure a [Provider Authentication Token](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html) and then create an ```app``` configuration, which will contain the token, as well as the topic (bundle-id) of your app and wheather it's using a production or sandbox environment. Assign the ```app``` configuration to the ```apn``` node and you are ready to send notifications. 21 | 22 | You can use the ```apn-notification``` node to set the properties of a notification, or you can send the values in the ```msg.notification``` object. You can also set the raw payload which will be sent to Apple in the ```msg.rawNotification``` object. More information available in the ```apn``` node info tab. 23 | 24 | The tokens must be provided in the ```msg.tokens``` object as an array. 25 | 26 | ## GCM 27 | 28 | The ```gcm``` node is used to send notifications to Android and iOS (if it's configured in Firebase) devices. For Chrome notifications, check out the ```web``` node. You will need to configure your GCM Api Key and assign it to the node. 29 | 30 | You can use the ```gcm-notification``` node to set the properties of a notification, or you can send the values in the ```msg.notification``` object. 31 | 32 | The recipient of the notification can be specified by setting one of the following keys: ```to```, ```topic```, ```condition```, ```registrationTokens``` or ```tokens```. 33 | 34 | More information available in the ```gcm``` node info tab. 35 | 36 | ## Web 37 | 38 | The ```web``` node is used to send notifications to Chrome, Firefox, Opera, and Samsung Internet browsers. For a list of supported versions of those browsers, check the [web-push](https://github.com/web-push-libs/web-push) page. 39 | 40 | Some browsers (Chrome and Opera) requires a GCM Api Key to send notifications, so you will need to configure it in the node. 41 | 42 | You can use the ```web-notification``` node to set the properties of a notification, or you can send the values in the ```msg.notification``` object. 43 | 44 | The device tokens must be provided in the ```msg.tokens``` object and they must contain the ```endpoint```, as well as the ```p256dh``` and the ```auth``` keys. 45 | 46 | More information available in the ```web``` node info tab. 47 | 48 | ## Additional information 49 | 50 | The ```apn```, ```gcm``` and ```web``` nodes will return the result in the ```msg.result``` key. 51 | 52 | ## Example flow 53 | 54 | You can find an example flow in ```exampleFlow.json```. 55 | 56 | ## Screenshots 57 | 58 | ![Screenshot 1](/screenshot1.PNG?raw=true "Screenshot 1") 59 | 60 | ## License 61 | 62 | Copyright 2017 Mihail Cristian Dumitru 63 | 64 | Licensed under the Apache License, Version 2.0 (the "License"); 65 | you may not use this file except in compliance with the License. 66 | You may obtain a copy of the License at 67 | 68 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 69 | 70 | Unless required by applicable law or agreed to in writing, software 71 | distributed under the License is distributed on an "AS IS" BASIS, 72 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 73 | See the License for the specific language governing permissions and 74 | limitations under the License. -------------------------------------------------------------------------------- /apn/apn-notification.js: -------------------------------------------------------------------------------- 1 | module.exports = function (RED) { 2 | function APNNotificationNode(n) { 3 | RED.nodes.createNode(this, n); 4 | 5 | this.content = n; 6 | 7 | var node = this; 8 | 9 | this.on('input', function (msg) { 10 | var payload = {}; 11 | 12 | node.status({}); 13 | 14 | try { 15 | 16 | if (node.content.title) { 17 | payload.title = node.content.title; 18 | } 19 | 20 | if (node.content.body) { 21 | payload.body = node.content.body; 22 | } 23 | 24 | if (node.content.sound) { 25 | payload.sound = node.content.sound; 26 | } 27 | 28 | if (node.content.badge) { 29 | payload.badge = Number(node.content.badge); 30 | } 31 | 32 | if (node.content.action) { 33 | payload.action = node.content.action; 34 | } 35 | 36 | if (node.content.category) { 37 | payload.category = node.content.category; 38 | } 39 | 40 | if (node.content.contentAvailable) { 41 | payload.contentAvailable = 1; 42 | } 43 | 44 | if (node.content.mutableContent) { 45 | payload.mutableContent = 1; 46 | } 47 | 48 | if (node.content.payload) { 49 | var data = {}; 50 | var keyValArray = JSON.parse(node.content.payload) 51 | for (var i = 0; i < keyValArray.length; i++) { 52 | switch (keyValArray[i].type) { 53 | case "str": 54 | var value = String(keyValArray[i].value) 55 | data[keyValArray[i].key] = value 56 | break 57 | case "num": 58 | var value = parseFloat(keyValArray[i].value) 59 | if (!Number.isNaN(value)) { 60 | data[keyValArray[i].key] = value 61 | } else { 62 | throw new Error("Could not parse " + keyValArray[i].value + " into a number.") 63 | } 64 | break 65 | case "bool": 66 | var value = keyValArray[i].value == true 67 | data[keyValArray[i].key] = value 68 | break 69 | case "json": 70 | var value = JSON.parse(keyValArray[i].value) 71 | data[keyValArray[i].key] = value 72 | break 73 | } 74 | } 75 | payload.payload = data; 76 | } 77 | 78 | if (node.content.expiry) { 79 | var units = node.content.expiry.split(" ").map(function (unit) { 80 | return Number(unit) 81 | }) 82 | var minutes = units[0] || 0; 83 | var hours = units[1] || 0; 84 | var days = units[2] || 0; 85 | var weeks = units[3] || 0; 86 | 87 | payload.expiry = Math.floor(Date.now() / 1000) 88 | + (60 * minutes) 89 | + (60 * 60 * hours) 90 | + (60 * 60 * 24 * days) 91 | + (60 * 60 * 24 * 7 * weeks) 92 | } 93 | 94 | if (node.content.urlArgs) { 95 | var urlArgs = JSON.parse(node.content.urlArgs); 96 | 97 | if (urlArgs.length > 0) { 98 | payload.urlArgs = urlArgs; 99 | } 100 | } 101 | 102 | if (node.content.priority) { 103 | payload.priority = node.content.priority; 104 | } 105 | 106 | msg.notification = payload; 107 | 108 | node.send(msg); 109 | 110 | } catch (err) { 111 | node.status({ fill: "red", shape: "dot", text: err.message }) 112 | } 113 | }); 114 | 115 | this.on('close', function () { 116 | node.status({}); 117 | }); 118 | } 119 | RED.nodes.registerType("apn-notification", APNNotificationNode); 120 | } -------------------------------------------------------------------------------- /web/web-notification.html: -------------------------------------------------------------------------------- 1 | 124 | 125 | 165 | 166 | 181 | 182 | -------------------------------------------------------------------------------- /exampleFlow.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "727cd3c5.76010c", 4 | "type": "tab", 5 | "label": "Push" 6 | }, 7 | { 8 | "id": "94c512b4.9589e", 9 | "type": "apn", 10 | "z": "727cd3c5.76010c", 11 | "name": "Demo iOS App Prod", 12 | "app": "4f846751.82f658", 13 | "x": 760, 14 | "y": 180, 15 | "wires": [ 16 | [ 17 | "7932dc64.d623a4" 18 | ] 19 | ] 20 | }, 21 | { 22 | "id": "f83f93a1.5ba51", 23 | "type": "apn-notification", 24 | "z": "727cd3c5.76010c", 25 | "name": "", 26 | "title": "", 27 | "body": "Hello, world!", 28 | "action": "", 29 | "badge": "42", 30 | "sound": "default", 31 | "contentAvailable": true, 32 | "mutableContent": "", 33 | "urlArgs": "", 34 | "category": "", 35 | "expiry": "* * 1 *", 36 | "priority": "10", 37 | "payload": "[{\"key\":\"item\",\"value\":\"37\",\"type\":\"num\"},{\"key\":\"category\",\"value\":\"2\",\"type\":\"num\"},{\"key\":\"name\",\"value\":\"Some random item\",\"type\":\"str\"}]", 38 | "x": 540, 39 | "y": 160, 40 | "wires": [ 41 | [ 42 | "e48becf2.02004" 43 | ] 44 | ] 45 | }, 46 | { 47 | "id": "2144f4b0.d1633c", 48 | "type": "gcm", 49 | "z": "727cd3c5.76010c", 50 | "name": "Demo Android App", 51 | "gcmConfiguration": "4114a855.7b8bf8", 52 | "x": 750, 53 | "y": 240, 54 | "wires": [ 55 | [ 56 | "7932dc64.d623a4" 57 | ] 58 | ] 59 | }, 60 | { 61 | "id": "3a0403e9.d5e4fc", 62 | "type": "gcm-notification", 63 | "z": "727cd3c5.76010c", 64 | "name": "", 65 | "title": "", 66 | "body": "Hello, world!", 67 | "sound": "default", 68 | "expiry": "* * 1 *", 69 | "priority": "high", 70 | "dryRun": "", 71 | "payload": "[{\"key\":\"item\",\"value\":\"37\",\"type\":\"str\"},{\"key\":\"category\",\"value\":\"2\",\"type\":\"str\"},{\"key\":\"name\",\"value\":\"Some random item\",\"type\":\"str\"}]", 72 | "x": 540, 73 | "y": 240, 74 | "wires": [ 75 | [ 76 | "2144f4b0.d1633c" 77 | ] 78 | ] 79 | }, 80 | { 81 | "id": "7d0466d6.7ea788", 82 | "type": "web", 83 | "z": "727cd3c5.76010c", 84 | "name": "", 85 | "gcmConfiguration": "", 86 | "x": 710, 87 | "y": 300, 88 | "wires": [ 89 | [ 90 | "7932dc64.d623a4" 91 | ] 92 | ] 93 | }, 94 | { 95 | "id": "c619e6f9.ff73b8", 96 | "type": "web-notification", 97 | "z": "727cd3c5.76010c", 98 | "name": "", 99 | "title": "", 100 | "body": "Hello, world!", 101 | "sound": "default", 102 | "payload": "[{\"key\":\"item\",\"value\":\"37\",\"type\":\"str\"},{\"key\":\"category\",\"value\":\"2\",\"type\":\"str\"},{\"key\":\"name\",\"value\":\"Some random item\",\"type\":\"str\"}]", 103 | "x": 540, 104 | "y": 300, 105 | "wires": [ 106 | [ 107 | "7d0466d6.7ea788" 108 | ] 109 | ] 110 | }, 111 | { 112 | "id": "e48becf2.02004", 113 | "type": "apn", 114 | "z": "727cd3c5.76010c", 115 | "name": "Demo iOS App Dev", 116 | "app": "4f7edc8a.cfacf4", 117 | "x": 750, 118 | "y": 120, 119 | "wires": [ 120 | [ 121 | "7932dc64.d623a4" 122 | ] 123 | ] 124 | }, 125 | { 126 | "id": "7932dc64.d623a4", 127 | "type": "debug", 128 | "z": "727cd3c5.76010c", 129 | "name": "", 130 | "active": true, 131 | "console": "false", 132 | "complete": "result", 133 | "x": 970, 134 | "y": 240, 135 | "wires": [] 136 | }, 137 | { 138 | "id": "e5c3b331.f2a51", 139 | "type": "inject", 140 | "z": "727cd3c5.76010c", 141 | "name": "", 142 | "topic": "", 143 | "payload": "", 144 | "payloadType": "date", 145 | "repeat": "", 146 | "crontab": "", 147 | "once": false, 148 | "x": 160, 149 | "y": 120, 150 | "wires": [ 151 | [ 152 | "97d5c689.6c0518" 153 | ] 154 | ] 155 | }, 156 | { 157 | "id": "97d5c689.6c0518", 158 | "type": "function", 159 | "z": "727cd3c5.76010c", 160 | "name": "iOS Dev Tokens", 161 | "func": "msg.tokens = [\n \n]\nreturn msg;", 162 | "outputs": 1, 163 | "noerr": 0, 164 | "x": 340, 165 | "y": 120, 166 | "wires": [ 167 | [ 168 | "f83f93a1.5ba51" 169 | ] 170 | ] 171 | }, 172 | { 173 | "id": "b94f107.b4396f", 174 | "type": "function", 175 | "z": "727cd3c5.76010c", 176 | "name": "iOS Prod Tokens", 177 | "func": "msg.tokens = [\n \n]\nreturn msg;", 178 | "outputs": 1, 179 | "noerr": 0, 180 | "x": 330, 181 | "y": 180, 182 | "wires": [ 183 | [] 184 | ] 185 | }, 186 | { 187 | "id": "5ed8262.917f0d8", 188 | "type": "function", 189 | "z": "727cd3c5.76010c", 190 | "name": "Android Tokens", 191 | "func": "msg.tokens = [\n \n]\nreturn msg;", 192 | "outputs": 1, 193 | "noerr": 0, 194 | "x": 340, 195 | "y": 240, 196 | "wires": [ 197 | [ 198 | "3a0403e9.d5e4fc" 199 | ] 200 | ] 201 | }, 202 | { 203 | "id": "ca5cc2ba.4f269", 204 | "type": "function", 205 | "z": "727cd3c5.76010c", 206 | "name": "Web Tokens", 207 | "func": "msg.tokens = [\n \n]\nreturn msg;", 208 | "outputs": 1, 209 | "noerr": 0, 210 | "x": 350, 211 | "y": 300, 212 | "wires": [ 213 | [ 214 | "c619e6f9.ff73b8" 215 | ] 216 | ] 217 | }, 218 | { 219 | "id": "6e2cf095.4ff6c", 220 | "type": "inject", 221 | "z": "727cd3c5.76010c", 222 | "name": "", 223 | "topic": "", 224 | "payload": "", 225 | "payloadType": "date", 226 | "repeat": "", 227 | "crontab": "", 228 | "once": false, 229 | "x": 160, 230 | "y": 180, 231 | "wires": [ 232 | [ 233 | "b94f107.b4396f" 234 | ] 235 | ] 236 | }, 237 | { 238 | "id": "5f91f727.449258", 239 | "type": "inject", 240 | "z": "727cd3c5.76010c", 241 | "name": "", 242 | "topic": "", 243 | "payload": "", 244 | "payloadType": "date", 245 | "repeat": "", 246 | "crontab": "", 247 | "once": false, 248 | "x": 160, 249 | "y": 240, 250 | "wires": [ 251 | [ 252 | "5ed8262.917f0d8" 253 | ] 254 | ] 255 | }, 256 | { 257 | "id": "2689df58.4d09", 258 | "type": "inject", 259 | "z": "727cd3c5.76010c", 260 | "name": "", 261 | "topic": "", 262 | "payload": "", 263 | "payloadType": "date", 264 | "repeat": "", 265 | "crontab": "", 266 | "once": false, 267 | "x": 160, 268 | "y": 300, 269 | "wires": [ 270 | [ 271 | "ca5cc2ba.4f269" 272 | ] 273 | ] 274 | }, 275 | { 276 | "id": "4f846751.82f658", 277 | "type": "apn-app", 278 | "z": "", 279 | "name": "Demo App", 280 | "token": "95bf22d2.9332b", 281 | "topic": "com.example.app", 282 | "production": true 283 | }, 284 | { 285 | "id": "4114a855.7b8bf8", 286 | "type": "gcm-configuration", 287 | "z": "", 288 | "name": "Demo GCM Project", 289 | "apiKey": "API_KEY" 290 | }, 291 | { 292 | "id": "4f7edc8a.cfacf4", 293 | "type": "apn-app", 294 | "z": "", 295 | "name": "Demo App", 296 | "token": "95bf22d2.9332b", 297 | "topic": "com.example.app", 298 | "production": false 299 | }, 300 | { 301 | "id": "95bf22d2.9332b", 302 | "type": "apn-token", 303 | "z": "", 304 | "name": "MyTeam", 305 | "key": "-----BEGIN PRIVATE KEY-----\n\n-----END PRIVATE KEY-----", 306 | "keyId": "KEY_ID", 307 | "teamId": "TEAM_ID" 308 | } 309 | ] -------------------------------------------------------------------------------- /gcm/gcm-notification.html: -------------------------------------------------------------------------------- 1 | 214 | 215 | 293 | 294 | 323 | 324 | -------------------------------------------------------------------------------- /apn/apn-notification.html: -------------------------------------------------------------------------------- 1 | 341 | 342 | 459 | 460 | 495 | 496 | --------------------------------------------------------------------------------