1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | var HTTP_412_MINIMUM_DELAY_MINUTES = 61;
|
19 | var ERROR_MINIMUM_DELAY_MINUTES = 5;
|
20 |
|
21 | var url = require('url'),
|
22 | http = require('http'),
|
23 | https = require('https');
|
24 |
|
25 | var Toast = function(options) {
|
26 | return new PushMessage('toast', '2', 'toast', options);
|
27 | };
|
28 |
|
29 | var LiveTile = function(options) {
|
30 | return new PushMessage('tile', '1', 'token', options);
|
31 | };
|
32 |
|
33 | var FlipTile = function(options) {
|
34 | if(!options){
|
35 | options = {};
|
36 | }
|
37 | options.tileTemplate = 'FlipTile';
|
38 | return new PushMessage('tile', '1', 'token', options);
|
39 | };
|
40 |
|
41 | var IconicTile = function(options) {
|
42 | options = options || {};
|
43 | options.tileTemplate = 'IconicTile';
|
44 | return new PushMessage('tile', '1', 'token', options);
|
45 | };
|
46 |
|
47 | var RawNotification = function(payload, options) {
|
48 | if (options === undefined) {
|
49 | options = payload;
|
50 | } else {
|
51 | options.payload = payload;
|
52 | }
|
53 | return new PushMessage('raw', '3', undefined, options);
|
54 | };
|
55 |
|
56 | function PushMessage(pushType, quickNotificationClass, targetName, options) {
|
57 | this.pushType = pushType;
|
58 | this.notificationClass = quickNotificationClass;
|
59 | this.targetName = targetName;
|
60 |
|
61 | if (options) {
|
62 | copyOfInterest(options, this, properties.ofInterest, this.pushType === 'tile');
|
63 | copyOfInterest(options, this, properties.ssl);
|
64 | copyOfInterest(options, this, properties.http);
|
65 | }
|
66 | }
|
67 |
|
68 | PushMessage.prototype.send = function(pushUri, callback) {
|
69 | var payload = this.getXmlPayload();
|
70 | var me = this;
|
71 |
|
72 | var options = url.parse(pushUri);
|
73 | options.method = 'POST';
|
74 | options.headers = {
|
75 | 'Content-Type': 'text/xml',
|
76 | 'Content-Length': Buffer.byteLength(payload).toString(),
|
77 | 'Accept': 'application/*',
|
78 | 'X-NotificationClass': this.notificationClass
|
79 | };
|
80 |
|
81 | if(this.targetName){
|
82 | options.headers['X-WindowsPhone-Target'] = this.targetName;
|
83 | }
|
84 |
|
85 | if (this.proxy) {
|
86 | var proxyOptions = url.parse(this.proxy);
|
87 | options.headers['Host'] = options.host;
|
88 | options.path = options.href;
|
89 | options.host = proxyOptions.host;
|
90 | options.hostname = proxyOptions.hostname;
|
91 | options.port = proxyOptions.port;
|
92 | }
|
93 |
|
94 | var result = { };
|
95 | var err;
|
96 | var protocol = http;
|
97 | if (options.protocol == 'https:') {
|
98 | protocol = https;
|
99 |
|
100 | copyOfInterest(me, options, properties.ssl);
|
101 |
|
102 |
|
103 | options.agent = false;
|
104 | }
|
105 |
|
106 | var req = protocol.request(options);
|
107 |
|
108 | req.on('response', function (message) {
|
109 | result.statusCode = message.statusCode;
|
110 |
|
111 | if (message.headers) {
|
112 | renameFieldsOfInterest(message.headers, result, {
|
113 | 'x-deviceconnectionstatus': 'deviceConnectionStatus',
|
114 | 'x-notificationstatus': 'notificationStatus',
|
115 | 'x-subscriptionstatus': 'subscriptionStatus'
|
116 | });
|
117 | }
|
118 |
|
119 |
|
120 | copyOfInterest(me, result, properties.ofInterest, this.pushType === 'tile');
|
121 |
|
122 | switch (message.statusCode) {
|
123 |
|
124 | case 412:
|
125 | result.minutesToDelay = HTTP_412_MINIMUM_DELAY_MINUTES;
|
126 | err = result;
|
127 | break;
|
128 |
|
129 |
|
130 | case 400:
|
131 | case 401:
|
132 | case 404:
|
133 | result.shouldDeleteChannel = true;
|
134 | err = result;
|
135 | break;
|
136 |
|
137 | case 403:
|
138 | err = result;
|
139 | err.innerError = 'Authenticated push notifications certificate problem.';
|
140 | break;
|
141 |
|
142 |
|
143 | case 405:
|
144 | err = result;
|
145 | break;
|
146 |
|
147 | case 406:
|
148 | err = result;
|
149 | err.innerError = 'Per-day throttling limit reached.';
|
150 | break;
|
151 |
|
152 | case 503:
|
153 | err = result;
|
154 | result.minutesToDelay = ERROR_MINIMUM_DELAY_MINUTES;
|
155 | err.innerError = 'The Push Notification Service is unable to process the request.';
|
156 | break;
|
157 | }
|
158 |
|
159 |
|
160 |
|
161 |
|
162 | message.resume();
|
163 |
|
164 | if (callback)
|
165 | callback(err, err === undefined ? result : undefined);
|
166 | });
|
167 |
|
168 | req.on('error', function(e)
|
169 | {
|
170 | result.minutesToDelay = ERROR_MINIMUM_DELAY_MINUTES;
|
171 | err = result;
|
172 | err.innerError = e;
|
173 |
|
174 | if (callback)
|
175 | callback(err);
|
176 | });
|
177 |
|
178 |
|
179 | req.write(payload);
|
180 | req.end();
|
181 | };
|
182 |
|
183 | function copyOfInterest(source, destination, fieldsOfInterest, allowNull) {
|
184 | if (source && destination && fieldsOfInterest && fieldsOfInterest.length) {
|
185 | for (var i = 0; i < fieldsOfInterest.length; i++) {
|
186 | var key = fieldsOfInterest[i];
|
187 | if (source[key] || (source[key] === null && allowNull === true)) {
|
188 | destination[key] = source[key];
|
189 | }
|
190 | }
|
191 | }
|
192 | }
|
193 |
|
194 | function renameFieldsOfInterest(source, destination, map) {
|
195 | if (source && destination && map) {
|
196 | for (var key in map) {
|
197 | var newKey = map[key];
|
198 | if (source[key]) {
|
199 | destination[newKey] = source[key];
|
200 | }
|
201 | }
|
202 | }
|
203 | }
|
204 |
|
205 | PushMessage.prototype.getXmlPayload = function() {
|
206 | this.validate();
|
207 | if (this.pushType == 'tile') {
|
208 | return tileToXml(this);
|
209 | } else if (this.pushType == 'toast') {
|
210 | return toastToXml(this);
|
211 | } else if (this.pushType == 'raw') {
|
212 | return this.payload;
|
213 | }
|
214 | };
|
215 |
|
216 | PushMessage.prototype.validate = function() {
|
217 | if (this.pushType != 'toast' && this.pushType != 'tile' && this.pushType != 'raw') {
|
218 | throw new Error("Only 'toast', 'tile' and 'raw' push types are currently supported.");
|
219 | }
|
220 | };
|
221 |
|
222 | function escapeXml(value) {
|
223 | if (value && value.replace) {
|
224 | value = value.replace(/\&/g,'&')
|
225 | .replace(/</g, '<')
|
226 | .replace(/>/g, '>')
|
227 | .replace(/"/g, '"');
|
228 | }
|
229 | return value;
|
230 | }
|
231 |
|
232 | function getPushHeader(type, attributes) {
|
233 | return '<?xml version="1.0" encoding="utf-8"?><wp:Notification xmlns:wp="WPNotification">' +
|
234 | startTag(type, attributes);
|
235 | }
|
236 |
|
237 | function getPushFooter(type) {
|
238 | return endTag(type) + endTag('Notification');
|
239 | }
|
240 |
|
241 | function startTag(tag, attributes, endInstead) {
|
242 | tag = '<' + (endInstead ? '/' : '') + 'wp:' + tag;
|
243 | if(!endInstead && attributes && attributes.length){
|
244 | attributes.forEach(function(pair){
|
245 | tag += ' ' + pair[0] + '="' + escapeXml(pair[1]) + '"';
|
246 | });
|
247 | }
|
248 | tag += '>';
|
249 | return tag;
|
250 | }
|
251 |
|
252 | function endTag(tag) {
|
253 | return startTag(tag, null, true);
|
254 | }
|
255 |
|
256 | function wrapValue(object, key, name) {
|
257 |
|
258 | if(object[key] === null){
|
259 | return startTag(name, [['Action', 'Clear']]) + endTag(name);
|
260 | } else {
|
261 | return object[key] ? startTag(name) + escapeXml(object[key]) + endTag(name) : '';
|
262 | }
|
263 | }
|
264 |
|
265 | function toastToXml(options) {
|
266 | var type = 'Toast';
|
267 | return getPushHeader(type) +
|
268 | wrapValue(options, 'text1', 'Text1') +
|
269 | wrapValue(options, 'text2', 'Text2') +
|
270 | wrapValue(options, 'param', 'Param') +
|
271 | getPushFooter(type);
|
272 | }
|
273 |
|
274 | function tileToXml(options) {
|
275 | var type = 'Tile';
|
276 | return getPushHeader(type, tileGetAttributes(options)) +
|
277 | wrapValue(options, 'backgroundImage', 'BackgroundImage') +
|
278 | wrapValue(options, 'count', 'Count') +
|
279 | wrapValue(options, 'title', 'Title') +
|
280 | wrapValue(options, 'backBackgroundImage', 'BackBackgroundImage') +
|
281 | wrapValue(options, 'backTitle', 'BackTitle') +
|
282 | wrapValue(options, 'backContent', 'BackContent') +
|
283 | wrapValue(options, 'smallBackgroundImage', 'SmallBackgroundImage') +
|
284 | wrapValue(options, 'wideBackgroundImage', 'WideBackgroundImage') +
|
285 | wrapValue(options, 'wideBackContent', 'WideBackContent') +
|
286 | wrapValue(options, 'wideBackBackgroundImage', 'WideBackBackgroundImage') +
|
287 | wrapValue(options, 'backgroundColor', 'BackgroundColor') +
|
288 | wrapValue(options, 'iconImage', 'IconImage') +
|
289 | wrapValue(options, 'smallIconImage', 'SmallIconImage') +
|
290 | wrapValue(options, 'wideContent1', 'WideContent1') +
|
291 | wrapValue(options, 'wideContent2', 'WideContent2') +
|
292 | wrapValue(options, 'wideContent3', 'WideContent3') +
|
293 | getPushFooter(type);
|
294 | }
|
295 |
|
296 | function tileGetAttributes(options){
|
297 | var attributes = [];
|
298 | if(options.tileTemplate){
|
299 | attributes.push(['Template', options.tileTemplate]);
|
300 | }
|
301 | if (options.id){
|
302 | attributes.push(['Id', options.id]);
|
303 | }
|
304 | return attributes;
|
305 | }
|
306 |
|
307 | function create(type, typeProperties, objectType, args) {
|
308 | var params = {};
|
309 | if (typeof args[0] === 'object') {
|
310 | var payload = Array.prototype.shift.apply(args);
|
311 |
|
312 |
|
313 |
|
314 | if (payload.smallbackgroundImage) {
|
315 | payload.smallBackgroundImage = payload.smallbackgroundImage;
|
316 | }
|
317 |
|
318 | copyOfInterest(payload, params, typeProperties, type === 'tile');
|
319 | copyOfInterest(payload, params, properties.ssl, false);
|
320 | copyOfInterest(payload, params, properties.http, false);
|
321 | }
|
322 | else {
|
323 |
|
324 | var i = 0;
|
325 | while ((typeof args[0] === 'string' || typeof args[0] === 'number') && i < typeProperties.length) {
|
326 | var item = Array.prototype.shift.apply(args);
|
327 | var key = typeProperties[i++];
|
328 | params[key] = item;
|
329 | }
|
330 | }
|
331 |
|
332 | if (type == 'toast' && typeof params.text1 !== 'string') {
|
333 | throw new Error('The text1 toast parameter must be set and a string.');
|
334 | }
|
335 |
|
336 | if (type == 'tile' && Object.keys(params).length === 0) {
|
337 | throw new Error('At least 1 tile parameter must be set.');
|
338 | }
|
339 |
|
340 | return new objectType(params);
|
341 | }
|
342 |
|
343 | function send(type, typeProperties, objectType, args) {
|
344 | var pushUri = Array.prototype.shift.apply(args);
|
345 |
|
346 | if (typeof pushUri !== 'string')
|
347 | throw new Error('The pushUri parameter must be the push notification channel URI string.');
|
348 |
|
349 | var callback = args[args.length - 1];
|
350 |
|
351 | if (callback && typeof callback !== 'function')
|
352 | throw new Error('The callback parameter, if specified, must be the callback function.');
|
353 |
|
354 | var instance = create(type, typeProperties, objectType, args);
|
355 | instance.send(pushUri, callback);
|
356 | }
|
357 |
|
358 | var properties = {
|
359 | http: [
|
360 | 'proxy'
|
361 | ],
|
362 | ssl: [
|
363 | 'pfx',
|
364 | 'key',
|
365 | 'passphrase',
|
366 | 'cert',
|
367 | 'ca',
|
368 | 'ciphers',
|
369 | 'rejectUnauthorized'
|
370 | ],
|
371 | toast: [
|
372 | 'text1',
|
373 | 'text2',
|
374 | 'param'
|
375 | ],
|
376 | tile: [
|
377 | 'backgroundImage',
|
378 | 'count',
|
379 | 'title',
|
380 | 'backBackgroundImage',
|
381 | 'backTitle',
|
382 | 'backContent',
|
383 | 'id'
|
384 | ],
|
385 | flipTile: [
|
386 | 'smallBackgroundImage',
|
387 | 'wideBackgroundImage',
|
388 | 'wideBackContent',
|
389 | 'wideBackBackgroundImage'
|
390 | ],
|
391 | iconicTile: [
|
392 | 'backgroundColor',
|
393 | 'count',
|
394 | 'title',
|
395 | 'iconImage',
|
396 | 'smallIconImage',
|
397 | 'wideContent1',
|
398 | 'wideContent2',
|
399 | 'wideContent3',
|
400 | 'id'
|
401 | ],
|
402 | ofInterest: [
|
403 | 'payload',
|
404 | 'pushType',
|
405 | 'tileTemplate'
|
406 | ]
|
407 | };
|
408 |
|
409 | properties.flipTile = properties.flipTile.concat(properties.tile);
|
410 | properties.ofInterest = properties.ofInterest.concat(
|
411 | properties.toast, properties.flipTile, properties.iconicTile);
|
412 |
|
413 |
|
414 | properties.ofInterest = properties.ofInterest.slice().sort().reduce(function (a, x) {
|
415 | if (a.slice(-1) != x) a.push(x);
|
416 | return a;
|
417 | }, []);
|
418 |
|
419 | module.exports = {
|
420 | sendTile: function () {
|
421 | send('tile', properties.tile, LiveTile, arguments);
|
422 | },
|
423 |
|
424 | sendFlipTile: function() {
|
425 | send('tile', properties.flipTile, FlipTile, arguments);
|
426 | },
|
427 |
|
428 | sendIconicTile: function() {
|
429 | send('tile', properties.iconicTile, IconicTile, arguments);
|
430 | },
|
431 |
|
432 | sendToast: function () {
|
433 | send('toast', properties.toast, Toast, arguments);
|
434 | },
|
435 |
|
436 | sendRaw: function () {
|
437 | send('raw', ['payload'], RawNotification, arguments);
|
438 | },
|
439 |
|
440 | createTile: function () {
|
441 | return create('tile', properties.tile, LiveTile, arguments);
|
442 | },
|
443 |
|
444 | createFlipTile: function() {
|
445 | return create('tile', properties.flipTile, FlipTile, arguments);
|
446 | },
|
447 |
|
448 | createIconicTile: function() {
|
449 | return create('tile', properties.iconicTile, IconicTile, arguments);
|
450 | },
|
451 |
|
452 | createToast: function () {
|
453 | return create('toast', properties.toast, Toast, arguments);
|
454 | },
|
455 |
|
456 | createRaw: function () {
|
457 | return create('raw', ['payload'], RawNotification, arguments);
|
458 | },
|
459 |
|
460 |
|
461 | Properties: properties
|
462 | };
|