1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 | var HTTP_412_MINIMUM_DELAY_MINUTES = 61;
|
25 | var ERROR_MINIMUM_DELAY_MINUTES = 5;
|
26 |
|
27 | var url = require('url');
|
28 | var http = require('http');
|
29 |
|
30 | var Toast = function(options) {
|
31 | return new PushMessage('toast', '2', 'toast', options);
|
32 | };
|
33 |
|
34 | var LiveTile = function(options) {
|
35 | return new PushMessage('tile', '1', 'token', options);
|
36 | };
|
37 |
|
38 | var 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 |
|
47 | function 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 |
|
57 | PushMessage.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 |
|
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 |
|
96 | copyOfInterest(me, result, propertiesOfInterest);
|
97 |
|
98 | switch (res.statusCode) {
|
99 |
|
100 | case 412:
|
101 | result.minutesToDelay = HTTP_412_MINIMUM_DELAY_MINUTES;
|
102 | err = result;
|
103 | break;
|
104 |
|
105 |
|
106 | case 400:
|
107 | case 401:
|
108 | case 404:
|
109 | result.shouldDeleteChannel = true;
|
110 | err = result;
|
111 | break;
|
112 |
|
113 |
|
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;
|
135 | err = result;
|
136 | err.error = e;
|
137 |
|
138 | if (callback)
|
139 | callback(err);
|
140 | });
|
141 | });
|
142 |
|
143 |
|
144 | req.write(payload);
|
145 | req.end();
|
146 | };
|
147 |
|
148 | function 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 |
|
159 | function 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 |
|
170 | PushMessage.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 |
|
181 | PushMessage.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 |
|
187 | function escapeXml(value) {
|
188 | if (value && value.replace) {
|
189 | value = value.replace(/\&/g,'&')
|
190 | .replace(/</g, '<')
|
191 | .replace(/>/g, '>')
|
192 | .replace(/"/g, '"');
|
193 | }
|
194 | return value;
|
195 | }
|
196 |
|
197 | function getPushHeader(type) {
|
198 | return '<?xml version="1.0" encoding="utf-8"?><wp:Notification xmlns:wp="WPNotification">' +
|
199 | startTag(type);
|
200 | }
|
201 |
|
202 | function getPushFooter(type) {
|
203 | return endTag(type) + endTag('Notification');
|
204 | }
|
205 |
|
206 | function startTag(tag, endInstead) {
|
207 | return '<' + (endInstead ? '/' : '') + 'wp:' + tag + '>';
|
208 | }
|
209 |
|
210 | function endTag(tag) {
|
211 | return startTag(tag, true);
|
212 | }
|
213 |
|
214 | function wrapValue(object, key, name) {
|
215 | return object[key] ? startTag(name) + escapeXml(object[key]) + endTag(name) : null;
|
216 | }
|
217 |
|
218 | function 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 |
|
227 | function 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 |
|
239 | exports.sendTile = function () {
|
240 | send('tile', tileProperties, LiveTile, arguments);
|
241 | }
|
242 |
|
243 | exports.sendToast = function () {
|
244 | send('toast', toastProperties, Toast, arguments);
|
245 | }
|
246 |
|
247 | exports.sendRawNotification = function () {
|
248 | send('raw', 'payload', RawNotification, arguments);
|
249 | }
|
250 |
|
251 | function 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 |
|
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 |
|
287 | var toastProperties = [
|
288 | 'text1',
|
289 | 'text2',
|
290 | 'param'
|
291 | ];
|
292 |
|
293 | var tileProperties = [
|
294 | 'backgroundImage',
|
295 | 'count',
|
296 | 'title',
|
297 | 'backBackgroundImage',
|
298 | 'backTitle',
|
299 | 'backContent'
|
300 | ];
|
301 |
|
302 | var propertiesOfInterest = toastProperties.concat(tileProperties);
|
303 | propertiesOfInterest.push('payload', 'pushType');
|
304 |
|
305 |
|
306 |
|
307 | exports.liveTile = LiveTile;
|
308 | exports.toast = Toast;
|
309 | exports.rawNotification = RawNotification;
|