UNPKG

9.94 kBJavaScriptView Raw
1// Copyright Jeff Wilcox
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15
16// NOTES:
17//
18// This library is designed for Windows Phone 7.1 and 8.0 OS. To send push to
19// a 7.0 device, the developer must not include the param value (toast) or
20// any of the advanced back background tile images (tiles).
21
22// Suggested fallback values if you're storing results in a document/db.
23// See also: http://msdn.microsoft.com/en-us/library/ff941100%28v=VS.92%29.aspx
24var HTTP_412_MINIMUM_DELAY_MINUTES = 61;
25var ERROR_MINIMUM_DELAY_MINUTES = 5;
26
27var url = require('url');
28var http = require('http');
29
30var Toast = function(options) {
31 return new PushMessage('toast', '2', 'toast', options);
32};
33
34var LiveTile = function(options) {
35 return new PushMessage('tile', '1', 'token', options);
36};
37
38var RawNotification = function(payload, options) {
39 if (options == undefined) {
40 options = payload;
41 } else {
42 options.payload = payload;
43 }
44 return new PushMessage('raw', '3', undefined, options);
45};
46
47function PushMessage(pushType, quickNotificationClass, targetName, options) {
48 this.pushType = pushType;
49 this.notificationClass = quickNotificationClass;
50 this.targetName = targetName;
51
52 if (options) {
53 copyOfInterest(options, this, propertiesOfInterest);
54 }
55}
56
57PushMessage.prototype.send = function(pushUri, callback) {
58 var payload = this.getXmlPayload();
59 var uriInfo = url.parse(pushUri);
60 var me = this;
61
62 var headers = {
63 'Content-Type': 'text/xml',
64 'Content-Length': Buffer.byteLength(payload),
65 'Accept': 'application/*',
66 'X-NotificationClass': this.notificationClass,
67 'X-WindowsPhone-Target': this.targetName
68 };
69
70 var options = {
71 headers: headers,
72 host: uriInfo.host,
73 port: uriInfo.protocol == "http:" ? 80 : 443,
74 path: uriInfo.pathname,
75 method: 'POST'
76 };
77
78 var result = { };
79 var err = undefined;
80
81 var req = http.request(options, function(res) {
82 res.setEncoding('utf8');
83 res.on('end', function() {
84 result.statusCode = res.statusCode;
85
86 // Store the important responses from MPNS.
87 if (res.headers) {
88 renameFieldsOfInterest(res.headers, result, {
89 'x-deviceconnectionstatus': 'deviceConnectionStatus',
90 'x-notificationstatus': 'notificationStatus',
91 'x-subscriptionstatus' : 'subscriptionStatus'
92 });
93 }
94
95 // Store the fields that were sent to make it easy to log.
96 copyOfInterest(me, result, propertiesOfInterest);
97
98 switch (res.statusCode) {
99 // The device is in an inactive state.
100 case 412:
101 result.minutesToDelay = HTTP_412_MINIMUM_DELAY_MINUTES; // Must be at least an hour.
102 err = result;
103 break;
104
105 // Invalid subscriptions.
106 case 400:
107 case 401:
108 case 404:
109 result.shouldDeleteChannel = true;
110 err = result;
111 break;
112
113 // Method Not Allowed (bug in this library)
114 case 405:
115 err = result;
116 break;
117
118 case 406:
119 err = result;
120 err.error = 'Per-day throttling limit reached.';
121 break;
122
123 case 503:
124 err = result;
125 result.minutesToDelay = ERROR_MINIMUM_DELAY_MINUTES;
126 err.error = 'The Push Notification Service is unable to process the request.';
127 break;
128 }
129
130 if (callback)
131 callback(err, err === undefined ? result : undefined);
132
133 }).on('error', function(e) {
134 result.minutesToDelay = ERROR_MINIMUM_DELAY_MINUTES; // Just a recommendation.
135 err = result;
136 err.error = e;
137
138 if (callback)
139 callback(err);
140 });
141 });
142
143 // Send the push notification to Microsoft.
144 req.write(payload);
145 req.end();
146};
147
148function copyOfInterest(source, destination, fieldsOfInterest) {
149 if (source && destination && fieldsOfInterest && fieldsOfInterest.length) {
150 for (var i = 0; i < fieldsOfInterest.length; i++) {
151 var key = fieldsOfInterest[i];
152 if (source[key]) {
153 destination[key] = source[key];
154 }
155 }
156 }
157}
158
159function renameFieldsOfInterest(source, destination, map) {
160 if (source && destination && map) {
161 for (var key in map) {
162 var newKey = map[key];
163 if (source[key]) {
164 destination[newKey] = source[key];
165 }
166 }
167 }
168}
169
170PushMessage.prototype.getXmlPayload = function() {
171 this.validate();
172 if (this.pushType == 'tile') {
173 return tileToXml(this);
174 } else if (this.pushType == 'toast') {
175 return toastToXml(this);
176 } else if (this.pushType == 'raw') {
177 return this.payload;
178 }
179};
180
181PushMessage.prototype.validate = function() {
182 if (this.pushType != 'toast' && this.pushType != 'tile' && this.pushType != 'raw') {
183 throw new Error("Only 'toast', 'tile' and 'raw' push types are currently supported.");
184 }
185};
186
187function escapeXml(value) {
188 if (value && value.replace) {
189 value = value.replace(/\&/g,'&amp;')
190 .replace(/</g, '&lt;')
191 .replace(/>/g, '&gt;')
192 .replace(/"/g, '&quot;');
193 }
194 return value;
195}
196
197function getPushHeader(type) {
198 return '<?xml version="1.0" encoding="utf-8"?><wp:Notification xmlns:wp="WPNotification">' +
199 startTag(type);
200}
201
202function getPushFooter(type) {
203 return endTag(type) + endTag('Notification');
204}
205
206function startTag(tag, endInstead) {
207 return '<' + (endInstead ? '/' : '') + 'wp:' + tag + '>';
208}
209
210function endTag(tag) {
211 return startTag(tag, true);
212}
213
214function wrapValue(object, key, name) {
215 return object[key] ? startTag(name) + escapeXml(object[key]) + endTag(name) : null;
216}
217
218function toastToXml(options) {
219 var type = 'Toast';
220 return getPushHeader(type) +
221 wrapValue(options, 'text1', 'Text1') +
222 wrapValue(options, 'text2', 'Text2') +
223 wrapValue(options, 'param', 'Param') +
224 getPushFooter(type);
225}
226
227function tileToXml(options) {
228 var type = 'Tile';
229 return getPushHeader(type) +
230 wrapValue(options, 'backgroundImage', 'BackgroundImage') +
231 wrapValue(options, 'count', 'Count') +
232 wrapValue(options, 'title', 'Title') +
233 wrapValue(options, 'backBackgroundImage', 'BackBackgroundImage') +
234 wrapValue(options, 'backTitle', 'BackTitle') +
235 wrapValue(options, 'backContent', 'BackContent') +
236 getPushFooter(type);
237}
238
239exports.sendTile = function () {
240 send('tile', tileProperties, LiveTile, arguments);
241}
242
243exports.sendToast = function () {
244 send('toast', toastProperties, Toast, arguments);
245}
246
247exports.sendRawNotification = function () {
248 send('raw', 'payload', RawNotification, arguments);
249}
250
251function send(type, typeProperties, objectType, args) {
252 var pushUri = Array.prototype.shift.apply(args);
253
254 if (typeof pushUri !== 'string')
255 throw new Error('The pushUri parameter must be the push notification channel URI string.');
256
257 var params = [];
258 if (typeof args[0] === 'object') {
259 var payload = Array.prototype.shift.apply(args);
260 copyOfInterest(payload, params, typeProperties);
261 }
262 else {
263 // assume parameters are provided as atomic, string arguments of the function call
264 var i = 0;
265 while ((typeof args[0] === 'string' || typeof args[0] === 'number') && i < typeProperties.length) {
266 var item = Array.prototype.shift.apply(args);
267 var key = typeProperties[i++];
268 params[key] = item;
269 }
270 }
271
272 if (type == 'toast' && typeof params.text1 !== 'string')
273 throw new Error('The text1 toast parameter must be set and a string.');
274
275 if (type == 'tile' && params.length == 0)
276 throw new Error('At least 1 tile parameter must be set.');
277
278 var callback = args[args.length - 1];
279
280 if (callback && typeof callback !== 'function')
281 throw new Error('The callback parameter, if specified, must be the callback function.');
282
283 var instance = new objectType(params);
284 instance.send(pushUri, callback);
285}
286
287var toastProperties = [
288 'text1',
289 'text2',
290 'param'
291];
292
293var tileProperties = [
294 'backgroundImage',
295 'count',
296 'title',
297 'backBackgroundImage',
298 'backTitle',
299 'backContent'
300];
301
302var propertiesOfInterest = toastProperties.concat(tileProperties);
303propertiesOfInterest.push('payload', 'pushType');
304
305// These object constructors are effectively deprecated. Consider using
306// sendToast, sendTile or sendRawNotification methods going forward.
307exports.liveTile = LiveTile;
308exports.toast = Toast;
309exports.rawNotification = RawNotification;