├── .gitignore ├── LICENSE ├── README.md ├── config.js ├── examples └── config │ ├── config.development.js │ ├── config.global.js │ └── config.production.js ├── index.js ├── lib └── messages.js ├── package.json └── private ├── .gitignore └── config.defaults.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013, Jardel Weyrich 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redis-pusher 2 | 3 | A Node.js application that consumes messages published on Redis channels and 4 | send them as Push Notifications via APNS or GCM. 5 | 6 | 1. Workflow 7 | 8 | a) Events: Sit and listen for messages published on the Redis subscribed 9 | channel(s). 10 | 11 | b) Processing: Convert each published message to the APNS or GCM message format 12 | and dispatch them to the APNS or GCM server. 13 | 14 | c) Feedback: Periodically connect to the APNS Feedback server, and receive a 15 | list of devices that have persistent delivery failures so that you can 16 | mark them as bad and refrain from sending new push notifications to them. 17 | 18 | 2. Sharding 19 | 20 | You can split the workload across multiple `redis-pusher` instances. 21 | To do that, configure `config.redis.channels` in your configuration file so 22 | that each `redis-pusher` instance subscribes to different channel(s). 23 | 24 | 3. Failover redundancy 25 | 26 | You may have multipe instances subscribed to the same channel(s) 27 | simultaneously. To do that, all your instances sharing the same channel(s) 28 | must specify the same value for `config.redis.lock.keyPrefix` and set 29 | `config.redis.failoverEnabled` to `true`. 30 | 31 | 4. Configuration 32 | 33 | Each configuration file may be considered an environment (e.g.: development, 34 | production, etc). A new configuration file can extend an existing 35 | configuration by adding these lines before anything else: 36 | 37 | var config = module.exports = require('./another_existing_file') 38 | var this_config_name = 'example'; 39 | config.loaded.push(this_config_name); 40 | 41 | To switch between environments (configurations), specify 42 | `NODE_ENV=` when running `redis-pusher`. Example: 43 | 44 | NODE_ENV=production node . 45 | 46 | The example above will load `./private/config.production.js`. 47 | 48 | 5. What `redis-pusher` does not attempt to do 49 | 50 | It does not process any of the APNS feedback messages. It also does not handle 51 | GCM replies that tell you the device is not (or no longer) registered. 52 | This is something specific to your scenario. For example, your scenario might 53 | involve an SQL table containing all registered devices (id, device_token, etc), 54 | so you could mark them as bad, or simply delete them. It's entirely up to you! 55 | 56 | - - - 57 | 58 | ### How can I test it? 59 | 60 | ##### Install the required tools 61 | 62 | 1. [Node.js](http://nodejs.org/) 63 | 2. [Redis](http://redis.io/) 64 | 65 | ##### Clone the repository 66 | 67 | $ git clone https://github.com/jweyrich/redis-pusher.git 68 | 69 | ##### Install the dependencies 70 | 71 | $ cd redis-pusher 72 | $ npm install 73 | 74 | ##### Test locally 75 | 76 | ###### Configure 77 | 78 | a) Copy your APNS certificates and keys to the private 79 | directory that resides within the project's directory: 80 | 81 | $ cp -i /path/to/your/apns_development.p12 \ 82 | /path/to/your/apns_production.p12 \ 83 | private/ 84 | 85 | b) Copy the example configuration files to the project's `private` 86 | directory: 87 | 88 | $ cp -ir examples/config/* private/ 89 | 90 | c) Make sure nobody else can read the contents of your private directory: 91 | 92 | $ chmod 700 private 93 | 94 | d) Edit your private configuration files according to your needs: 95 | 96 | config.redis.host -- The host your Redis instance is running. 97 | config.redis.port -- The port your Redis instance is listening. 98 | config.redis.pass -- The password to your Redis instance. 99 | Please, set to '' if you don't need it. 100 | config.redis.channels -- Which redis channels `redis-pusher` will listen to. 101 | IMPORTANT: The current version of `redis-pusher` can only distinguish 102 | APNS messages from GCM messages using the channel name. Channel names 103 | for APNS must contain one of ['apns','ios','iphone','ipad'], and 104 | channel names for GCM must contain one of ['gcm','android']. 105 | "ABSURD!" you scream. Right! I just haven't had the time to come up 106 | with a clean solution. 107 | 108 | config.apns.certificate -- The APNS certificate file in .p12 (PKCS #12) format. 109 | config.apns.passphrase -- The passphrase for your APNS certificate file. 110 | Please, set to `undefined` if you don't need it. 111 | 112 | config.gcm.options.key -- Your GCM API key. 113 | 114 | ###### Run it 115 | 116 | $ NODE_ENV=development node . & 117 | $ redis-cli 118 | redis> publish development:push:ios '{ "identifier": "a-unique-identifier", "tokens": [ , , ... ], "expires": 300, "badge": 1, "sound": "default", "alert": "You have a new message" }' 119 | redis> publish development:push:android '{ "identifier": "another-unique-identifier", "registrationId": [ [ , , ... ], "collapseKey": "status", "delayWhileIdle": false, "timeToLive": 300, "data": { "key1": "foo", "key2": "bar" } }' 120 | 121 | ##### Message format for iOS 122 | 123 | message { 124 | identifier: [string] -- Required. Unique identifier. 125 | token: [string or array of string] -- Required. The APNS device token of a recipient device, or 126 | an array of them for sending to 1 or more (up to ???). 127 | expires: [number] -- Seconds from now. 128 | badge: [number] 129 | sound: [string] 130 | alert: [string] 131 | payload: [object] 132 | retryLimit: [number] -- Optional. The maximum number of retries if an error occurs 133 | when sending a notification. A value of 0 will attempt 134 | sending only once (0 retries). 135 | } 136 | 137 | ##### Message format for Android 138 | 139 | message { 140 | identifier: [string] -- Required. Unique identifier. 141 | registrationId: [string or array of string] -- Required. The GCM registration ID of a recipient 142 | device, or an array of them for sending to 1 or 143 | more devices (up to 1000). When you send a message 144 | to multiple registration IDs, that is called a 145 | multicast message. 146 | collapseKey: [string] -- Optional. If there's an older message with the 147 | same collapseKey and registration ID, the older 148 | message will be discarded and the new one will 149 | take its place. 150 | delayWhileIdle: [boolean] -- Optional. Default is false. If the device is 151 | connected but idle, the message will still be 152 | delivered right away unless the delay_while_idle 153 | flag is set to true. Otherwise, it will be stored 154 | in the GCM servers until the device is awake. 155 | timeToLive: [number:0..2419200] -- Optional. How long (in seconds) the message should 156 | be kept on GCM storage if the device is offline. 157 | Requests that don't contain this field default to 158 | the maximum period of 4 weeks. When a message 159 | times out, it will be discarded from the GCM 160 | storage. 161 | data: [object] -- Optional. Custom data. 162 | } 163 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | var environment = process.env.NODE_ENV || 'development'; 2 | var config = require("./private/config." + environment); 3 | config.environment = environment; 4 | 5 | var setup_apns_options = function () { 6 | // gateway -> gateway.options 7 | config.apns.gateway.options = config.apns.gateway; 8 | // feedback -> feedback.options 9 | config.apns.feedback.options = config.apns.feedback; 10 | // gateway.address -> gateway.options.gateway 11 | config.apns.gateway.options.gateway = config.apns.gateway.address; 12 | // certificate -> gateway.pfx 13 | config.apns.gateway.pfx = config.apns.certificate; 14 | // certificate -> feedback.pfx 15 | config.apns.feedback.pfx = config.apns.certificate; 16 | // passphrase -> gateway.passphrase 17 | config.apns.gateway.passphrase = config.apns.passphrase; 18 | // passphrase -> feedback.passphrase 19 | config.apns.feedback.passphrase = config.apns.passphrase; 20 | }; 21 | 22 | setup_apns_options(); 23 | 24 | console.log('Running in ' + config.environment + ' [' + config.loaded.join(' -> ') + ']'); 25 | 26 | module.exports = config; 27 | -------------------------------------------------------------------------------- /examples/config/config.development.js: -------------------------------------------------------------------------------- 1 | // 2 | // DEVELOPMENT CONFIGURATIONS 3 | // 4 | 5 | var config = module.exports = require('./config.global'); 6 | var this_config_name = 'development'; 7 | config.loaded.push(this_config_name); 8 | 9 | // 10 | // Redis 11 | // 12 | config.redis.channels = [ 'development:push:ios', 'development:push:android' ]; 13 | 14 | // 15 | // APNS 16 | // 17 | config.apns.certificate = 'private/aps_development.p12'; 18 | config.apns.passphrase = ''; 19 | config.apns.gateway.address = 'gateway.sandbox.push.apple.com'; // Development 20 | config.apns.feedback.address = 'feedback.sandbox.push.apple.com'; // Development 21 | 22 | // 23 | // GCM 24 | // 25 | config.gcm.options.key = ''; // API key 26 | -------------------------------------------------------------------------------- /examples/config/config.global.js: -------------------------------------------------------------------------------- 1 | // 2 | // GLOBAL CONFIGURATIONS 3 | // 4 | 5 | var config = module.exports = require('./config.defaults'); 6 | var this_config_name = 'global'; 7 | config.loaded.push(this_config_name); 8 | 9 | // 10 | // Redis 11 | // 12 | config.redis.failoverEnabled = false; 13 | config.redis.host = '127.0.0.1'; 14 | config.redis.port = 6379; 15 | config.redis.pass = ''; 16 | config.redis.lock.keyPrefix = 'push_lock_'; 17 | -------------------------------------------------------------------------------- /examples/config/config.production.js: -------------------------------------------------------------------------------- 1 | // 2 | // PRODUCTION CONFIGURATIONS 3 | // 4 | 5 | var config = module.exports = require('./config.global'); 6 | var this_config_name = 'production'; 7 | config.loaded.push(this_config_name); 8 | 9 | // 10 | // Core 11 | // 12 | config.catchExceptions = true; 13 | 14 | // 15 | // Redis 16 | // 17 | config.redis.channels = [ 'production:push:ios' ]; 18 | 19 | // 20 | // APNS 21 | // 22 | config.apns.certificate = 'private/aps_production.p12'; 23 | config.apns.passphrase = ''; 24 | config.apns.gateway.address = 'gateway.push.apple.com'; // Development 25 | config.apns.feedback.address = 'feedback.push.apple.com'; // Development 26 | 27 | // 28 | // GCM 29 | // 30 | config.gcm.options.key = ''; // API key 31 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | $ NODE_ENV=development node . 3 | $ NODE_ENV=production node . 4 | */ 5 | 6 | var _ = require('lodash') 7 | , notify = require('push-notify') 8 | , apn = require('apn') 9 | , redis = require("redis") 10 | , util = require('util') 11 | , RedisLockingWorker = require("redis-locking-worker") 12 | , messages = require("./lib/messages") 13 | , config = require("./config"); 14 | 15 | // 16 | // REDIS MAIN CLIENT 17 | // 18 | var redisClient = redis.createClient(config.redis.port, config.redis.host); 19 | if (config.redis.pass) 20 | redisClient.auth(config.redis.pass); 21 | redisClient.on('error', function (err) { 22 | console.error("[redis-client] " + err); 23 | }).on('connect', function () { 24 | console.log("[redis-client] Connected"); 25 | }).on('close', function (why) { 26 | console.log("[redis-client] " + why); 27 | }); 28 | 29 | // 30 | // REDIS SUBSCRIPTION CLIENT 31 | // 32 | var redisSubscriber = redis.createClient(config.redis.port, config.redis.host); 33 | if (config.redis.pass) 34 | redisSubscriber.auth(config.redis.pass); 35 | redisSubscriber.on('error', function (err) { 36 | console.error("[redis-subscriber] " + err); 37 | }).on('connect', function () { 38 | console.log("[redis-subscriber] Connected"); 39 | redisSubscriber.subscribe(config.redis.channels, function () {}); 40 | }).on('close', function (why) { 41 | console.log("[redis-subscriber] " + why); 42 | }).on('subscribe', function (channel, count) { 43 | console.log("[redis-subscriber] Subscribed to '%s', %d total subscriptions", channel, count); 44 | }).on('unsubscribe', function (channel, count) { 45 | console.log("[redis-subscriber] Unsubscribed from '%s', %d total subscriptions", channel, count); 46 | if (count === 0) { 47 | redisSubscriber.end(); 48 | } 49 | }).on('message', function (channel, message) { 50 | console.log("[redis-subscriber] Received message: '%s'", message); 51 | // Error handling elided for brevity 52 | var msg = buildMessageBasedOnChannel(channel, message); 53 | if (msg) { 54 | var compiled = msg.compile(); 55 | if (compiled) 56 | processMessage(msg); 57 | else 58 | console.warn('Invalid message?'); 59 | } 60 | }); 61 | 62 | function buildMessageBasedOnChannel (channel, message) { 63 | var chan = channel.toLowerCase(); 64 | if (chan.indexOf('gcm') != -1 || chan.indexOf('android') != -1) 65 | return new messages.GCMMessage(message); 66 | if (chan.indexOf('apns') != -1 || chan.indexOf('ios') != -1 || chan.indexOf('iphone') != -1 || chan.indexOf('ipad') != -1) 67 | return new messages.APNSMessage(message); 68 | return undefined; 69 | } 70 | 71 | // 72 | // APNS GATEWAY 73 | // 74 | var apnsGateway = new notify.apn.Sender(config.apns.gateway.options); 75 | apnsGateway.on('error', function (err) { 76 | console.error("[apns-gateway] " + err); 77 | }).on('socketError', function (err) { 78 | console.error("[apns-gateway] " + err); 79 | }).on('timeout', function (err) { 80 | console.error("[apns-gateway] Timeout"); 81 | }).on('transmissionError', function (errCode, notification, device) { 82 | console.error("[apns-gateway] Transmission error (code %d) for recipient '%s'", errCode, device); 83 | }).on('connected', function (count) { 84 | console.log("[apns-gateway] Connected, %d total sockets", count); 85 | }).on('disconnected', function (count) { 86 | console.log("[apns-gateway] Disconnected, %d total sockets", count); 87 | }).on('transmitted', function (notification, recipient) { 88 | console.log("[apns-gateway] Transmitted '%s' to device '%s'", notification, recipient); 89 | }); 90 | 91 | // 92 | // APNS FEEDBACK 93 | // 94 | var apnsFeedback = new apn.Feedback(config.apns.feedback.options); 95 | apnsFeedback.on('error', function (err) { 96 | // Emitted when an error occurs initialising the module. Usually caused by failing to load the certificates. 97 | // This is most likely an unrecoverable error. 98 | console.error("[apns-feedback] " + err); 99 | }).on('feedbackError', function (err) { 100 | // Emitted when an error occurs receiving or processing the feedback and in the case of a socket error occurring. 101 | // These errors are usually informational and node-apn will automatically recover. 102 | console.error("[apns-feedback] " + err); 103 | }).on('feedback', function (feedbackData) { 104 | feedbackData.forEach(function (item) { 105 | var time = item.time; 106 | var device = item.device; 107 | 108 | // Do something with item.device and item.time; 109 | console.log("[apns-feedback] Should remove device: '%s'", device); 110 | }); 111 | }); 112 | 113 | // 114 | // GCM SENDER 115 | // 116 | var gcmSender = new notify.gcm.Sender(config.gcm.options); 117 | gcmSender.on('error', function (err) { 118 | console.error("[gcm-sender] " + err); 119 | }).on('transmissionError', function (error, registrationId) { 120 | console.error("[gcm-sender] Transmission error (%s) for recipient '%s'", error, registrationId); 121 | }).on('updated', function (result, registrationId) { 122 | console.log("[gcm-sender] Registration ID needs to be updated: '%s'", registrationId); 123 | }).on('transmitted', function (result, registrationId) { 124 | console.log("[gcm-sender] Transmitted '%s' to device '%s'", result, registrationId); 125 | }); 126 | 127 | function processMessage(message) { 128 | var worker = new RedisLockingWorker({ 129 | 'client': redisClient, 130 | 'lockKey' : config.redis.lock.keyPrefix + message.identifier, 131 | 'statusLevel' : RedisLockingWorker.StatusLevels.Verbose, 132 | 'lockTimeout' : config.redis.lock.lockTimeout, 133 | 'maxAttempts' : config.redis.lock.maxAttempts 134 | }); 135 | worker.on("acquired", function (lastAttempt) { 136 | console.log("[redis-client] Acquired lock %s", worker.lockKey); 137 | message.dispatch(); 138 | if (config.redis.failoverEnabled) 139 | worker.done(lastAttempt); 140 | else { 141 | console.log("[redis-client] Work complete. Deleting lock %s", worker.lockKey); 142 | worker.done(true); 143 | } 144 | worker = undefined; 145 | }); 146 | worker.on("locked", function () { 147 | console.log("[redis-client] Someone else acquired the lock %s", worker.lockKey); 148 | }); 149 | worker.on("error", function (error) { 150 | console.error("[redis-client] Error from lock %s: %j", worker.lockKey, error); 151 | }); 152 | worker.on("status", function (message) { 153 | console.log("[redis-client] Status message from lock %s: %s", worker.lockKey, message); 154 | }); 155 | worker.acquire(); 156 | } 157 | 158 | messages.APNSMessage.prototype.dispatch = function () { 159 | console.log("[apns-gateway] Sending notification '%s' to device(s) [ '%s' ]", 160 | this.identifier, this.token.join(", ")); 161 | 162 | // The APNS connection is defined/initialized elsewhere 163 | apnsGateway.send(this); 164 | }; 165 | 166 | messages.GCMMessage.prototype.dispatch = function () { 167 | console.log("[gcm-sender] Sending notification '%s' to device(s) [ '%s' ]", 168 | this.identifier, this.registrationId.join(", ")); 169 | 170 | // The APNS connection is defined/initialized elsewhere 171 | try { 172 | gcmSender.send(this); 173 | } catch (err) { 174 | console.error("[gcm-sender] node-gcm error: %s", err); 175 | } 176 | }; 177 | 178 | if (config.catchExceptions) { 179 | process.on('uncaughtException', function (err) { 180 | console.error('Caught exception: ' + err); 181 | }); 182 | } 183 | 184 | process.on('exit', function () { 185 | console.log('Dumping redis dataset to disk...'); 186 | if (redisClient.connected) { 187 | redisClient.save(); 188 | } 189 | console.log('Exiting.'); 190 | }); 191 | -------------------------------------------------------------------------------- /lib/messages.js: -------------------------------------------------------------------------------- 1 | 2 | function APNSMessage (data) { 3 | // identifier: [string] -- Required. Unique identifier. 4 | // token: [string or array of string] -- Required. The APNS device token of a recipient device, or an array of them for sending to 1 or more (up to ???). 5 | // expiry: [number] -- Note that it expects seconds from now, not the actual timestamp. 6 | // badge: [number] 7 | // sound: [string] 8 | // alert: [string] 9 | // payload: [object] 10 | 11 | var parsed = {}; 12 | if (data !== undefined && data !== null) { 13 | try { 14 | parsed = JSON.parse(data); 15 | } catch (exception) { 16 | console.error("Invalid JSON: " + data); 17 | } 18 | } 19 | 20 | this.identifier = parsed.identifier || undefined; 21 | this.token = parsed.token || undefined; 22 | this.expiry = parsed.expiry || 0; 23 | // Convert seconds from now to the actual timestamp. 24 | if (this.expiry) 25 | this.expiry = Math.floor(Date.now() / 1000) + this.expiry; 26 | this.badge = parsed.badge || undefined; 27 | this.sound = parsed.sound || undefined; 28 | this.alert = parsed.alert || undefined; 29 | this.payload = parsed.payload || {}; 30 | this.retryLimit = parsed.retryLimit || undefined; 31 | } 32 | 33 | APNSMessage.prototype.isTokenValid = function (token) { 34 | var object = token; 35 | var isString = typeof object === 'string'; 36 | var isArrayOfStrings = Array.isArray(object); 37 | var isValid = true; 38 | 39 | if (isArrayOfStrings) { 40 | for (o in object) { 41 | if (typeof o !== 'string') { 42 | console.error('APNSMessage has an invalid token: ' + o); 43 | isValid = false; 44 | break; 45 | } 46 | } 47 | } 48 | 49 | return isValid && (isString || isArrayOfStrings); 50 | }; 51 | 52 | APNSMessage.prototype.compile = function () { 53 | var valid = this.isTokenValid(this.token); 54 | if (!valid) 55 | return null; 56 | 57 | // If this.token is not an Array, convert it to Array. 58 | if (!Array.isArray(this.token)) { 59 | this.token = [ this.token ]; 60 | } 61 | 62 | return this; 63 | }; 64 | 65 | function GCMMessage (data) { 66 | // identifier: [string] -- Required. Unique identifier. 67 | // registrationId: [string or array of string] -- Required. The GCM registration ID of a recipient device, or an array of them for sending to 1 or more devices (up to 1000). When you send a message to multiple registration IDs, that is called a multicast message. 68 | // collapseKey: [string] -- Optional. If there's an older message with the same collapseKey and registration ID, the older message will be discarded and the new one will take its place. 69 | // delayWhileIdle: [boolean] -- Optional. Default is false. If the device is connected but idle, the message will still be delivered right away unless the delay_while_idle flag is set to true. Otherwise, it will be stored in the GCM servers until the device is awake. 70 | // timeToLive: [number:0..2419200] -- Optional. How long (in seconds) the message should be kept on GCM storage if the device is offline. Requests that don't contain this field default to the maximum period of 4 weeks. When a message times out, it will be discarded from the GCM storage. 71 | // data: [object] -- Optional. Custom payload. 72 | 73 | var parsed = {}; 74 | if (data !== undefined && data !== null) { 75 | try { 76 | parsed = JSON.parse(data); 77 | } catch (exception) { 78 | console.error("Invalid JSON: " + data); 79 | } 80 | } 81 | 82 | this.identifier = parsed.identifier || undefined; 83 | this.registrationId = parsed.registrationId || undefined; 84 | this.collapseKey = parsed.collapseKey || undefined; 85 | this.delayWhileIdle = parsed.delayWhileIdle || undefined; 86 | this.timeToLive = parsed.timeToLive || undefined; 87 | this.data = parsed.data || undefined; 88 | } 89 | 90 | GCMMessage.prototype.isRegistrationIdValid = function (registrationId) { 91 | var object = registrationId; 92 | var isString = typeof object === 'string'; 93 | var isArrayOfStrings = Array.isArray(object); 94 | var isValid = true; 95 | 96 | if (isArrayOfStrings) { 97 | for (o in object) { 98 | if (typeof o !== 'string') { 99 | console.error('GCMMessage has an invalid registrationId: ' + o); 100 | isValid = false; 101 | break; 102 | } 103 | } 104 | } 105 | 106 | return isValid && (isString || isArrayOfStrings); 107 | }; 108 | 109 | GCMMessage.prototype.compile = function () { 110 | var valid = this.isRegistrationIdValid(this.registrationId); 111 | if (!valid) 112 | return null; 113 | 114 | // If this.registrationId is not an Array, convert it to Array. 115 | if (!Array.isArray(this.registrationId)) { 116 | this.registrationId = [ this.registrationId ]; 117 | } 118 | 119 | return this; 120 | }; 121 | 122 | module.exports.APNSMessage = APNSMessage; 123 | module.exports.GCMMessage = GCMMessage; 124 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis-pusher", 3 | "version": "0.2.2", 4 | "description": "A Node.js application that consumes messages published on Redis channels and send them as Push Notifications via APNS or GCM.", 5 | "keywords": [ "redis", "sharding", "apple", "push", "notification", "iOS", "apns", "android", "gcm" ], 6 | "author": "Jardel Weyrich ", 7 | "main": "index.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "git@github.com:jweyrich/redis-pusher.git" 11 | }, 12 | "dependencies": { 13 | "push-notify": "~0.3.0", 14 | "apn": "~1.3.2", 15 | "lodash": "~2.4.1", 16 | "redis": "~0.8.4", 17 | "redis-locking-worker": "git://github.com/yahoo/redis-locking-worker.git" 18 | }, 19 | "engines": { 20 | "node": "~0.10.12", 21 | "npm": "~1.2.32" 22 | }, 23 | "license": "MIT" 24 | } 25 | -------------------------------------------------------------------------------- /private/.gitignore: -------------------------------------------------------------------------------- 1 | * -------------------------------------------------------------------------------- /private/config.defaults.js: -------------------------------------------------------------------------------- 1 | // 2 | // DEFAULT CONFIGURATIONS 3 | // 4 | 5 | var config = module.exports = {}; 6 | config.loaded = config.loaded ? config.loaded : []; 7 | var this_config_name = 'defaults'; 8 | config.loaded.push(this_config_name); 9 | 10 | // 11 | // Core 12 | // 13 | config.catchExceptions = false; 14 | 15 | // 16 | // Redis 17 | // 18 | config.redis = {}; 19 | config.redis.failoverEnabled = false; 20 | config.redis.host = ''; 21 | config.redis.port = 6379; 22 | config.redis.pass = ''; 23 | //config.redis.retry_max_delay = 1000 * 300; // 300 seconds. 24 | config.redis.channels = [ '' ]; 25 | config.redis.lock = {}; 26 | config.redis.lock.keyPrefix = ''; 27 | config.redis.lock.lockTimeout = 5000; // 5 seconds. 28 | config.redis.lock.maxAttempts = 5; 29 | 30 | // 31 | // APNS 32 | // 33 | config.apns = {}; 34 | config.apns.certificate = ''; // Only .p12 files 35 | config.apns.passphrase = ''; 36 | config.apns.gateway = {}; 37 | config.apns.gateway.connectionTimeout = 1000 * 240; // 240 seconds. 38 | config.apns.gateway.address = ''; 39 | config.apns.feedback = {}; 40 | config.apns.feedback.batchFeedback = true; 41 | config.apns.feedback.interval = 300; // Pooling interval in seconds. 42 | config.apns.feedback.address = ''; 43 | 44 | // 45 | // GCM 46 | // 47 | config.gcm = {}; 48 | config.gcm.options = {}; 49 | config.gcm.options.key = ''; 50 | config.gcm.options.retries = 3; 51 | config.gcm.options.proxy = undefined; // An HTTP proxy to be used. Supports proxy Auth with Basic Auth by embedding the auth info in the uri. 52 | config.gcm.options.timeout = 180000; // Integer containing the number of milliseconds to wait for a request to respond before aborting the request. 53 | --------------------------------------------------------------------------------