UNPKG

11 kBJavaScriptView Raw
1var assert = require('assert');
2var inherits = require('util').inherits;
3var EventEmitter = require('events').EventEmitter;
4var format = require('util').format;
5var async = require('async');
6var providers = require('./providers');
7var loopback = require('loopback');
8var NodeCache = require('node-cache');
9var debug = require('debug')('loopback:component:push:push-manager');
10
11var Installation = require('../models/installation');
12var Notification = require('../models/notification');
13
14/*!
15 * Exports a function to bootstrap PushManager
16 * @param {Object} settings An object to configure APNS
17 */
18module.exports = PushManager;
19
20/**
21 * @class
22 * @param {Object} settings The push settings
23 * @returns {PushManager}
24 * @constructor
25 */
26function PushManager(settings) {
27 if (!(this instanceof PushManager)) {
28 return new PushManager(settings);
29 }
30
31 settings = settings || {};
32
33 this.settings = settings;
34 this._defineDependencies();
35 this.ttlInSeconds = this.settings.ttlInSeconds || 0;
36 this.checkPeriodInSeconds = this.settings.checkPeriodInSeconds || 0;
37 this.applicationsCache = new NodeCache({ stdTTL: this.ttlInSeconds, checkperiod: this.checkPeriodInSeconds });
38}
39
40inherits(PushManager, EventEmitter);
41
42/**
43 * Define the dependent models lazily to avoid race condition
44 * as the push data source can be created before installation/notification
45 * models are defined.
46 *
47 * @private
48 */
49PushManager.prototype._defineDependencies = function () {
50
51 Object.defineProperties(this,
52 {
53 Installation: {
54 get: function () {
55 if (!this._Installation) {
56 this._Installation = loopback.getModel(this.settings.installation)
57 || loopback.getModelByType(Installation);
58 }
59 return this._Installation;
60 },
61 set: function (installation) {
62 this._Installation = installation;
63 }
64 },
65 Notification: {
66 get: function () {
67 if (!this._Notification) {
68 this._Notification = loopback.getModel(this.settings.notification)
69 || loopback.getModelByType(Notification);
70 }
71 return this._Notification;
72 },
73 set: function (notification) {
74 this._Notification = notification;
75 }
76 },
77 Application: {
78 get: function () {
79 if (!this._Application) {
80 this._Application = loopback.getModel(this.settings.application)
81 || loopback.getModelByType(loopback.Application);
82 }
83 return this._Application;
84 },
85 set: function (application) {
86 this._Application = application;
87 }
88 }
89
90 });
91};
92
93/**
94 * Registry of providers
95 * Key: device type, e.g. 'ios' or 'android'
96 * Value: constructor function, e.g providers.ApnsProvider
97 */
98PushManager.providers = {
99 ios: providers.ApnsProvider,
100 android: providers.GcmProvider
101};
102
103/**
104 * Configure push notification for a given device type. Return null when
105 * no provider is registered for the device type.
106 * @param {String} deviceType The device type
107 * @param {Object} pushSettings The push settings
108 * @returns {*}
109 */
110PushManager.prototype.configureProvider = function (deviceType, pushSettings) {
111 var Provider = PushManager.providers[deviceType];
112 if (!Provider) {
113 return null;
114 }
115
116 var provider = new Provider(pushSettings);
117 provider.on('devicesGone', function(deviceTokens) {
118 this.Installation.destroyAll({
119 deviceType: deviceType,
120 deviceToken: { inq: deviceTokens }
121 });
122 }.bind(this));
123
124 provider.on('error', function(err) {
125 this.emit('error', err);
126 }.bind(this));
127
128 return provider;
129};
130
131/**
132 * Lookup or set up push notification service for the given appId
133 * @param {String} appId The application id
134 * @returns {*}
135 */
136PushManager.prototype.configureApplication = function (appId, deviceType, cb) {
137 assert.ok(cb, 'callback should be defined');
138 var self = this;
139 var msg;
140
141 // Check the cache first
142 var cacheApp = self.applicationsCache.get(appId);
143 if (cacheApp[appId] && cacheApp[appId][deviceType]) {
144 return process.nextTick(function () {
145 cb(null, cacheApp[appId][deviceType]);
146 });
147 }
148
149 // Look up the application object by id
150 self.Application.findById(appId, function (err, application) {
151 if (err) {
152 debug('Cannot find application with id %j: %s',
153 appId, err.stack || err.message);
154 return cb(err);
155 }
156
157 if (!application) {
158 msg = format(
159 'Cannot configure push notifications - unknown application id %j',
160 appId);
161 debug('Error: %s', msg);
162
163 err = new Error(msg);
164 err.details = { appId: appId };
165 return cb(err);
166 }
167
168 var pushSettings = application.pushSettings;
169 if (!pushSettings) {
170 msg = format(
171 'No push settings configured for application %j (id: %j)',
172 application.name, application.id);
173 debug('Error: %s', msg);
174
175 err = new Error(msg);
176 err.details = {
177 application: {
178 id: application.id,
179 name: application.name
180 }
181 };
182 return cb(err);
183 }
184
185 debug(
186 'Setting up push notification for application id %j deviceType %j',
187 application.id,
188 deviceType
189 );
190
191 var provider = self.configureProvider(deviceType, pushSettings);
192
193 if (!provider) {
194 msg = 'There is no provider registered for deviceType ' + deviceType;
195 debug('Error: %s', msg);
196 err = new Error(msg);
197 err.details = {
198 deviceType: deviceType
199 };
200 return cb(err);
201 }
202
203 cacheApp[deviceType] = provider;
204 self.applicationsCache.set(appId, cacheApp);
205 cb(null, provider);
206 });
207};
208
209
210/**
211 * Push a notification to the device with the given registration id.
212 * @param {Object} installationId Registration id created by call to Installation.create().
213 * @param {Notification} notification The notification to send.
214 * @param {function(Error=)} cb
215 */
216PushManager.prototype.notifyById = function (installationId, notification, cb) {
217 assert.ok(cb, 'callback should be defined');
218 var self = this;
219 self.Installation.findById(installationId, function (err, installation) {
220 if (err) return cb(err);
221 if (!installation) {
222 var msg = 'Installation id ' + installationId + ' not found';
223 debug('notifyById failed: ' + msg);
224 err = new Error(msg);
225 err.details = { installationId: installationId };
226 return cb(err);
227 }
228 self.notify(installation, notification, cb);
229 });
230};
231
232/**
233 * Push a notification to all installations matching the given query.
234 * @param {Object} installationQuery Installation query, e.g.
235 * `{ appId: 'iCarsAppId', userId: 'jane.smith.id' }`
236 * @param {Notification} notification The notification to send.
237 * @param {function(Error=)} cb
238 */
239PushManager.prototype.notifyByQuery = function(installationQuery, notification, cb) {
240 assert.ok(cb, 'callback should be defined');
241 var self = this;
242 var filter = { where: installationQuery };
243 self.Installation.find(filter, function(err, installationList) {
244 if (err) return cb(err);
245 async.each(
246 installationList,
247 function(installation, next) {
248 self.notify(installation, notification, next);
249 },
250 cb
251 );
252 });
253};
254
255/**
256 * Push a notification to the given installation. This is a low-level function
257 * used by the other higher-level APIs like `notifyById` and `notifyByQuery`.
258 * @param {Installation} installation Installation instance - the recipient.
259 * @param {Notification} notification The notification to send.
260 * @param {function(Error=)} cb
261 */
262PushManager.prototype.notify = function(installation, notification, cb) {
263 assert(cb, 'callback should be defined');
264
265 if(!(typeof notification === 'object' && notification)) {
266 return cb(new Error('notification must be an object'));
267 }
268
269 var appId = installation.appId;
270 var deviceToken = installation.deviceToken;
271 var deviceType = installation.deviceType;
272
273 // Normalize the notification from a plain object
274 // for remote calls
275 if(!(notification instanceof this.Notification)) {
276 notification = new this.Notification(notification);
277 }
278
279 // Populate the deviceType/deviceToken to avoid validation errors
280 // as both properties are required
281 notification.deviceType = deviceType || notification.deviceType;
282 notification.deviceToken = deviceToken || notification.deviceToken;
283
284 if (!notification.isValid()) {
285 return cb(new loopback.ValidationError(notification));
286 }
287
288 this.configureApplication(
289 appId,
290 deviceType,
291 function(err, provider) {
292 if (err) return cb(err);
293
294 debug('Sending notification: ', deviceType, deviceToken, notification);
295 provider.pushNotification(notification, deviceToken);
296 cb();
297 }
298 );
299};
300
301 /**
302 * Push notification to installations for given devices tokens, device type and app.
303 *
304 * @param {appId} application id
305 * @param {deviceType} type of device (android, ios )
306 * @param {deviceTokens} device tokens of recipients.
307 * @param {Notification} notification The notification to send.
308 * @param {function(Error=)} cb
309 */
310PushManager.prototype.notifyMany = function(appId, deviceType, deviceTokens, notification, cb) {
311 assert(appId, 'appId should be defined');
312 assert(deviceType, 'deviceType should be defined')
313 assert(cb, 'callback should be defined');
314
315 if(!(typeof notification === 'object' && notification)) {
316 return cb(new Error('notification must be an object'));
317 }
318
319 if(!(typeof deviceTokens === 'object' && deviceTokens && deviceTokens.length > 0)) {
320 return cb(new Error('deviceTokens must be an array'));
321 }
322
323 // Normalize the notification from a plain object
324 // for remote calls
325 if(!(notification instanceof this.Notification)) {
326 notification = new this.Notification(notification);
327 if (!notification.isValid()) {
328 return cb(new loopback.ValidationError(notification));
329 }
330 }
331
332 this.configureApplication(
333 appId,
334 deviceType,
335 function(err, provider) {
336 if (err) { return cb(err); }
337
338 provider.pushNotification(notification, deviceTokens);
339 cb();
340 }
341 );
342};
343
344/*!
345 * Configure the remoting attributes for a given function
346 * @param {Function} fn The function
347 * @param {Object} options The options
348 * @private
349 */
350function setRemoting(fn, options) {
351 options = options || {};
352 for (var opt in options) {
353 if (options.hasOwnProperty(opt)) {
354 fn[opt] = options[opt];
355 }
356 }
357 fn.shared = true;
358}
359
360setRemoting(PushManager.prototype.notifyByQuery, {
361 description: 'Send a push notification by installation query',
362 accepts: [
363 {arg: 'deviceQuery', type: 'object', description: 'Installation query', http: {source: 'query'}},
364 {arg: 'notification', type: 'object', description: 'Notification', http: {source: 'body'}}
365 ],
366 returns: {arg: 'data', type: 'object', root: true},
367 http: {verb: 'post', path: '/'} // The url will be POST /push
368});