1 | var assert = require('assert');
|
2 | var inherits = require('util').inherits;
|
3 | var EventEmitter = require('events').EventEmitter;
|
4 | var format = require('util').format;
|
5 | var async = require('async');
|
6 | var providers = require('./providers');
|
7 | var loopback = require('loopback');
|
8 | var NodeCache = require('node-cache');
|
9 | var debug = require('debug')('loopback:component:push:push-manager');
|
10 |
|
11 | var Installation = require('../models/installation');
|
12 | var Notification = require('../models/notification');
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | module.exports = PushManager;
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 | function 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 |
|
40 | inherits(PushManager, EventEmitter);
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 | PushManager.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 |
|
95 |
|
96 |
|
97 |
|
98 | PushManager.providers = {
|
99 | ios: providers.ApnsProvider,
|
100 | android: providers.GcmProvider
|
101 | };
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 | PushManager.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 |
|
133 |
|
134 |
|
135 |
|
136 | PushManager.prototype.configureApplication = function (appId, deviceType, cb) {
|
137 | assert.ok(cb, 'callback should be defined');
|
138 | var self = this;
|
139 | var msg;
|
140 |
|
141 |
|
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 |
|
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 |
|
212 |
|
213 |
|
214 |
|
215 |
|
216 | PushManager.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 |
|
234 |
|
235 |
|
236 |
|
237 |
|
238 |
|
239 | PushManager.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 |
|
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 | PushManager.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 |
|
274 |
|
275 | if(!(notification instanceof this.Notification)) {
|
276 | notification = new this.Notification(notification);
|
277 | }
|
278 |
|
279 |
|
280 |
|
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 |
|
303 |
|
304 |
|
305 |
|
306 |
|
307 |
|
308 |
|
309 |
|
310 | PushManager.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 |
|
324 |
|
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 |
|
346 |
|
347 |
|
348 |
|
349 |
|
350 | function 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 |
|
360 | setRemoting(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: '/'}
|
368 | });
|