Gitter migration: Point people to app.gitter.im (rollout pt. 1)
[gitter.git] / modules / push-gateways / lib / ios / ios-notification-gateway.js
blob1a7a317b102087c1261d9c20a28550dce48d3bea
1 'use strict';
3 var apn = require('apn');
4 var debug = require('debug')('gitter:infra:ios-notification-gateway');
5 var env = require('gitter-web-env');
6 var Promise = require('bluebird');
7 var EventEmitter = require('events');
8 var path = require('path');
10 var iosNotificationGenerator = require('./ios-notification-generator');
11 var logger = env.logger.get('push-notifications');
12 var config = env.config;
13 var errorReporter = env.errorReporter;
15 var rootDirname = path.resolve(__dirname, '..', '..', '..', '..');
17 var ERROR_DESCRIPTIONS = {
18   0: 'No errors encountered',
19   1: 'Processing error',
20   2: 'Missing device token',
21   3: 'Missing topic',
22   4: 'Missing payload',
23   5: 'Invalid token size',
24   6: 'Invalid topic size',
25   7: 'Invalid payload size',
26   8: 'Invalid token',
27   255: 'None (unknown)'
30 var connections = {
31   APPLE: createConnection('Prod', true),
32   'APPLE-DEV': createConnection('Dev')
35 function resolveCertConfig(key) {
36   var relative = config.get(key);
37   if (!relative) return;
39   return path.resolve(rootDirname, relative);
42 function sendNotificationToDevice(notificationType, notificationDetails, device) {
43   var appleNotification = iosNotificationGenerator(notificationType, notificationDetails, device);
44   if (!appleNotification) return false;
46   var deviceToken = new apn.Device(device.appleToken);
48   var connection = connections[device.deviceType];
50   if (connection === false) {
51     return false;
52   } else if (!connection) {
53     return Promise.reject(new Error('unknown device type: ' + device.deviceType));
54   }
56   connection.pushNotification(appleNotification, deviceToken);
58   // timout needed to ensure that the push notification packet is sent.
59   // if we dont, SIGINT will kill notifications before they have left.
60   // until apn uses proper callbacks, we have to guess that it takes a second.
61   return Promise.resolve(true).delay(1000);
64 function createConnection(suffix, isProduction) {
65   debug('ios push notification gateway (%s) starting', suffix);
67   var certConfigKey = 'apn:cert' + suffix;
68   var cert = resolveCertConfig(certConfigKey);
69   var keyConfigKey = 'apn:key' + suffix;
70   var key = resolveCertConfig(keyConfigKey);
72   if (!cert || !key) {
73     logger.warn(
74       'ios push notification gateway (' +
75         suffix +
76         ') missing config, ' +
77         certConfigKey +
78         ':' +
79         cert +
80         ' ' +
81         keyConfigKey +
82         ':' +
83         key
84     );
85     return false;
86   }
88   var connection = new apn.Connection({
89     cert: cert,
90     key: key,
91     production: isProduction,
92     connectionTimeout: 60000
93   });
95   connection.on('error', function(err) {
96     logger.error('ios push notification gateway (' + suffix + ') experienced an error', {
97       error: err.message
98     });
99     errorReporter(err, { apnEnv: suffix }, { module: 'ios-notification-gateway' });
100   });
102   connection.on('socketError', function(err) {
103     logger.error('ios push notification gateway (' + suffix + ') experienced a socketError', {
104       error: err.message
105     });
106     errorReporter(err, { apnEnv: suffix }, { module: 'ios-notification-gateway' });
107   });
109   connection.on('transmissionError', function(errCode, notification, device) {
110     var err = new Error('apn transmission error ' + errCode + ': ' + ERROR_DESCRIPTIONS[errCode]);
111     if (errCode === 8) {
112       logger.warn(
113         'ios push notification gateway (' +
114           suffix +
115           ') invalid device token for "' +
116           suffix +
117           '". Need to remove the following device sometime:',
118         { device: device }
119       );
120     } else {
121       logger.error('ios push notification gateway (' + suffix + ')', { error: err.message });
122       errorReporter(err, { apnEnv: suffix }, { module: 'ios-notification-gateway' });
123     }
124   });
126   return connection;
129 function sendBadgeUpdateToDevice(device, badge) {
130   if (!device || !device.appleToken) return false;
132   var deviceToken = new apn.Device(device.appleToken);
133   var connection = connections[device.deviceType];
135   if (connection === false) {
136     return false;
137   } else if (!connection) {
138     return false;
139   }
141   var note = new apn.Notification();
142   note.badge = badge;
144   connection.pushNotification(note, deviceToken);
146   // timout needed to ensure that the push notification packet is sent.
147   // if we dont, SIGINT will kill notifications before they have left.
148   // until apn uses proper callbacks, we have to guess that it takes a second.
149   return Promise.resolve(true).delay(1000);
152 function createFeedbackEmitterForEnv(suffix, isProduction) {
153   var emitter = new EventEmitter();
155   try {
156     debug('ios push notification feedback listener (%s) starting', suffix);
158     var certConfigKey = 'apn:cert' + suffix;
159     var cert = resolveCertConfig(certConfigKey);
160     var keyConfigKey = 'apn:key' + suffix;
161     var key = resolveCertConfig(keyConfigKey);
163     if (!cert || !key) {
164       logger.warn(
165         'ios push notification feedback (' +
166           suffix +
167           ') missing config, ' +
168           certConfigKey +
169           ':' +
170           cert +
171           ' ' +
172           keyConfigKey +
173           ':' +
174           key
175       );
176       return emitter;
177     }
179     var feedback = new apn.Feedback({
180       cert: cert,
181       key: key,
182       interval: config.get('apn:feedbackInterval'),
183       batchFeedback: true,
184       production: isProduction
185     });
187     feedback.on('feedback', function(item) {
188       var deviceTokens = item.map(function(item) {
189         return item.device.token;
190       });
192       emitter.emit('deregister', deviceTokens);
193     });
195     feedback.on('error', function(err) {
196       logger.error('ios push notification feedback (' + suffix + ') experienced an error', {
197         error: err.message
198       });
199       errorReporter(err, { apnEnv: suffix }, { module: 'ios-notification-gateway' });
200     });
202     feedback.on('feedbackError', function(err) {
203       logger.error('ios push notification feedback (' + suffix + ') experienced a feedbackError', {
204         error: err.message
205       });
206       errorReporter(err, { apnEnv: suffix }, { module: 'ios-notification-gateway' });
207     });
208   } catch (err) {
209     logger.error('Unable to start feedback service (' + suffix + ')', { exception: err });
210     errorReporter(err, { apnEnv: suffix }, { module: 'ios-notification-gateway' });
211   }
213   return emitter;
216 function createFeedbackEmitter() {
217   /* Only create the feedback listeners for the current environment */
218   switch (config.get('NODE_ENV') || 'dev') {
219     case 'prod':
220       return createFeedbackEmitterForEnv('Prod', true);
222     case 'beta':
223     case 'dev':
224       return createFeedbackEmitterForEnv('Dev');
225   }
227   return new EventEmitter();
230 module.exports = {
231   sendNotificationToDevice: Promise.method(sendNotificationToDevice),
232   sendBadgeUpdateToDevice: Promise.method(sendBadgeUpdateToDevice),
233   createFeedbackEmitter: createFeedbackEmitter