Skip to content
This repository
Browse code

Lots of Doc. Some Notification helper methods.

  • Loading branch information...
commit 5530d7d31053d933cbff064a35b763c44f5160fa 1 parent 8490ab0
Andrew Naylor authored June 16, 2012
89  README.md
Source Rendered
@@ -15,7 +15,7 @@ Via [npm][]:
15 15
 
16 16
 	$ npm install apn
17 17
 	
18  
-As a submodule of your project
  18
+As a submodule of your project (you will also need to install [q][q])
19 19
 
20 20
 	$ git submodule add http://github.com/argon/node-apn.git apn
21 21
 	$ git submodule update --init
@@ -30,35 +30,39 @@ As a submodule of your project
30 30
 - Notification
31 31
 - Device
32 32
 - Feedback
33  
-- errors
  33
+- Errors
34 34
 
35 35
 ### Connecting
36 36
 Create a new connection to the gateway server using a dictionary of options. The defaults are listed below:
37 37
 
38 38
 	var options = {
39  
-		cert: 'cert.pem',                 /* Certificate file */
40  
-		certData: null,                   /* Optional: if supplied uses this instead of Certificate File */
41  
-		key:  'key.pem',                  /* Key file */
42  
-		keyData: null,                    /* Optional: if supplied uses this instead of Key file */
43  
-		passphrase: null,                 /* Optional: A passphrase for the Key file */
  39
+		cert: 'cert.pem',                 /* Certificate file path */
  40
+		certData: null,                   /* String or Buffer containing certificate data, if supplied uses this instead of cert file path */
  41
+		key:  'key.pem',                  /* Key file path */
  42
+		keyData: null,                    /* String or Buffer containing key data, as certData */
  43
+		passphrase: null,                 /* A passphrase for the Key file */
  44
+		ca: null						  /* String or Buffer of CA data to use for the TLS connection */
44 45
 		gateway: 'gateway.push.apple.com',/* gateway address */
45 46
 		port: 2195,                       /* gateway port */
46 47
 		enhanced: true,                   /* enable enhanced format */
47  
-		errorCallback: undefined,         /* Callback when error occurs */
48  
-		cacheLength: 5                    /* Number of notifications to cache for error purposes */
  48
+		errorCallback: undefined,         /* Callback when error occurs function(err,notification) */
  49
+		cacheLength: 100                  /* Number of notifications to cache for error purposes */
49 50
 	};
50 51
 
51 52
 	var apnsConnection = new apns.Connection(options);
  53
+	
  54
+**Important:** In a development environment you must set `gateway` to `gateway.sandbox.push.apple.com`.
52 55
 
53 56
 ### Sending a notification
54  
-To send a notification first create a `Device` object. Pass it the device token as either a hexadecimal string, or alternatively as a `Buffer` object containing the binary token, setting the second argument to `false`.
  57
+To send a notification first create a `Device` object. Pass it the device token as either a hexadecimal string, or alternatively as a `Buffer` object containing the token in binary form.
55 58
 
56  
-	var myDevice = new apns.Device(token /*, ascii=true*/);
  59
+	var myDevice = new apns.Device(token);
57 60
 
58  
-Next create a notification object and set parameters. See the [payload documentation][pl] for more details
  61
+Next, create a notification object and set parameters. See the [payload documentation][pl] for more details.
59 62
 
60 63
 	var note = new apns.Notification();
61 64
 	
  65
+	note.expiry = 60;
62 66
 	note.badge = 3;
63 67
 	note.sound = "ping.aiff";
64 68
 	note.alert = "You have a new message";
@@ -67,32 +71,41 @@ Next create a notification object and set parameters. See the [payload documenta
67 71
 	
68 72
 	apnsConnection.sendNotification(note);
69 73
 	
  74
+As of version 1.2.0 it is also possible to use a set of methods provided by Notification object (`setAlertText`, `setActionLocKey`, `setLocKey`, `setLocArgs`, `setLaunchImage`) to aid the creation of the alert parameters. For applications which provide Newsstand capability there is a new boolean parameter `note.newsstandAvailable` to specify `content-available` in the payload.
  75
+
70 76
 The above options will compile the following dictionary to send to the device:
71 77
 
72 78
 	{"messageFrom":"Caroline","aps":{"badge":3,"sound":"ping.aiff","alert":"You have a new message"}}
73 79
 	
74  
-\* N.B.: If you wish to send notifications containing emoji or other multi-byte characters you will need to set `note.encoding = 'ucs2'`. This tells node to send the message with 16bit characters, however it also means your message payload will be limited to 127 characters.
  80
+**\*N.B.:** If you wish to send notifications containing emoji or other multi-byte characters you will need to set `note.encoding = 'ucs2'`. This tells node to send the message with 16bit characters, however it also means your message payload will be limited to 127 characters.
75 81
 	
76 82
 ### Handling Errors
77 83
 
78  
-If the enhanced binary interface is enabled and an error occurs when sending a message then subsequent messages will be automatically resent* and the connection will be re-established. If an `errorCallback` is also specified in the connection options then it will be invoked with 2 arguments.
  84
+If the enhanced binary interface is enabled and an error occurs - as defined in Apple's documentation - when sending a message, then subsequent messages will be automatically resent* and the connection will be re-established. If an `errorCallback` is also specified in the connection options then it will be invoked with 2 arguments `(err, notification)`
  85
+
  86
+If a notification fails to be sent because a connection error occurs then the `errorCallback` will be called for each notification waiting for the connection which failed. In this case the first parameter will be an Error object instead of an error number.
  87
+
  88
+`errorCallback` will be called in 3 situations with the parameters shown.
79 89
 
80  
-1. The error number as returned from Apple. This can be compared to the predefined values in the `Errors` object.
81  
-1. The notification object as it existed when the notification was converted and sent to the server.
  90
+1. The notification has been rejected by Apple (or determined to have an invalid device token or payload before sending) for one of the reasons shown in Table 5-1 [here][errors] `errorCallback(errorCode, notification)`
  91
+1. A notification has been rejected by Apple but it has been removed from the cache so it is not possible to identify which. In this case subsequent notifications may be lost. **If this happens you should consider increasing your `cacheLength` value to prevent data loss** `errorCallback(255, null)`
  92
+1. A connection error has occurred before the notification can be sent. `errorCallback(Error object, notification)`
  93
+
  94
+**\*N.B.:** The `cacheLength` option for the connection specifies the number of sent notifications which will be cached, on a FIFO basis for error handling purposes. If `cacheLength` is not set to a large enough value, then in high volume environments, a notification - possibly including some subsequent notifications - may be removed from the cache before Apple returns an error associated with it. In this case the `errorCallback` will still be called, but with a `null` notification and error code 255. If this happens you should consider increasing `cacheLength` to prevent losing notifications. All the notifications still residing in the cache will be resent automatically.
82 95
 
83  
-\* N.B.: The `cacheLength` option specifies the number of sent notifications which will be cached for error handling purposes. At present if more than the specified number of notifications have been sent between the incorrect notification being sent and the error being received then no resending will occur. This is only envisaged within very high volume environments and a higher cache number might be desired.
84 96
 ### Setting up the feedback service
85 97
 
86 98
 Apple recommends checking the feedback service periodically for a list of devices for which there were failed delivery attempts.
87 99
 
88  
-Using the `Feedback` object it is possible to periodically query the server for the list. You should provide a function which will accept two arguments, the `time` returned by the server (epoch time) and a `Device` object containing the device token. You can also set the query interval in seconds. Again the default options are shown below.
  100
+Using the `Feedback` object it is possible to periodically query the server for the list. You should provide a function `feedback` which will accept two arguments, the `time` returned by the server (epoch time) and a `Buffer` object containing the device token. You can also set the query interval in seconds. The default options are shown below.
89 101
 
90 102
 	var options = {
91 103
 		cert: 'cert.pem',                   /* Certificate file */
92  
-		certData: null,                     /* Certificate file contents */
  104
+		certData: null,                     /* Certificate file contents (String|Buffer) */
93 105
 		key:  'key.pem',                    /* Key file */
94  
-		keyData: null,                      /* Key file contents */
95  
-		passphrase: null,                   /* Optional: A passphrase for the Key file */
  106
+		keyData: null,                      /* Key file contents (String|Buffer) */
  107
+		passphrase: null,                   /* A passphrase for the Key file */
  108
+		ca: null,							/* Certificate authority data to pass to the TLS connection */
96 109
 		address: 'feedback.push.apple.com', /* feedback address */
97 110
 		port: 2196,                         /* feedback port */
98 111
 		feedback: false,                    /* enable feedback service, set to callback */
@@ -101,11 +114,17 @@ Using the `Feedback` object it is possible to periodically query the server for
101 114
 
102 115
 	var feedback = new apns.Feedback(options);
103 116
 
  117
+This will automatically start a timer to check with Apple every `interval` seconds. You can cancel the interval by calling `feedback.cancel()`. If you do not wish to have the service automatically queried then set `interval` to 0 and use `feedback.start()`.
  118
+
  119
+**Important:** In a development environment you must set `address` to `feedback.sandbox.push.apple.com`.
  120
+
  121
+More information about the feedback service can be found in the [feedback service documentation][fs].
  122
+
104 123
 ## Converting your APNs Certificate
105 124
 
106  
-After requesting the certificate from Apple export your private key as a .p12 file and download the .cer file from the iOS Provisioning Portal.
  125
+After requesting the certificate from Apple, export your private key as a .p12 file and download the .cer file from the iOS Provisioning Portal.
107 126
 
108  
-Now in the directory containing cert.cer and key.p12 execute the following commands to generate your .pem files:
  127
+Now, in the directory containing cert.cer and key.p12 execute the following commands to generate your .pem files:
109 128
 
110 129
 	$ openssl x509 -in cert.cer -inform DER -outform PEM -out cert.pem
111 130
 	$ openssl pkcs12 -in key.p12 -out key.pem -nodes
@@ -114,12 +133,10 @@ If you are using a development certificate you may wish to name them differently
114 133
 
115 134
 ## Credits
116 135
 
117  
-Written and maintained by [Andrew Naylor][mphys].
  136
+Written and maintained by [Andrew Naylor][andrewnaylor].
118 137
 
119 138
 Contributors: [Ian Babrou][bobrik], [dgthistle][dgthistle], [Keith Larsen][keithnlarsen], [Mike P][mypark]
120 139
 
121  
-Special thanks to [Ben Noordhuis][bnoordhuis] for `invoke_after` code.
122  
-
123 140
 ## License
124 141
 
125 142
 Released under the MIT License
@@ -138,22 +155,40 @@ all copies or substantial portions of the Software.
138 155
 
139 156
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
140 157
 
  158
+[errors]:https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW4 "The Binary Interface and Notification Formats"
141 159
 [pl]: https://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1 "Local and Push Notification Programming Guide: Apple Push Notification Service"
142  
-[mphys]: http://mphys.com
  160
+[fs]:https://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW3 "Communicating With APS"
  161
+[andrewnaylor]: http://andrewnaylor.co.uk
143 162
 [bnoordhuis]: http://bnoordhuis.nl
144 163
 [npm]: https://github.com/isaacs/npm
145 164
 [bobrik]: http://bobrik.name
146 165
 [dgthistle]: https://github.com/dgthistle
147 166
 [keithnlarsen]: https://github.com/keithnlarsen
148 167
 [mypark]: https://github.com/mypark
  168
+[q]: https://github.com/kriskowal/q
149 169
 
150 170
 ## Changelog
151 171
 
  172
+1.2.0:
  173
+
  174
+* Complete rewrite of the connection handling.
  175
+* [q][q] is now required.
  176
+* Change in the error handling logic. When a notification errors and it cannot be found in the cache, then all notifications in the cache will be resent instead of being discarded.
  177
+* `errorCallback` will also be invoked for connection errors.
  178
+* New methods on `Notification` to aid settings the alert properties.
  179
+* `content-available` can now be set for Newsstand applications by setting the `newsstandAvailable` property on the Notification object.
  180
+* `Notification` objects now have a `.clone(device)` method to assist you in sending the same notification to multiple devices.
  181
+* Included some js-doc tags in the source.
  182
+* Device object now provides a `.toString()` method to return the hex representation of the device token.
  183
+* Fixes #23, #28, #32, #34, #35, #40, #42
  184
+
152 185
 1.1.7:
  186
+
153 187
 * Fixes a problem with sockets being closed on transmission error causing EPIPE errors in node.
154 188
 * Issues #29, #30
155 189
 
156 190
 1.1.6:
  191
+
157 192
 * Fixes a regression from v1.1.5 causing connections to stall and messages to not be sent.
158 193
 
159 194
 1.1.5:
106  lib/connection.js
@@ -5,23 +5,39 @@ var q    = require('q');
5 5
 var tls  = require('tls');
6 6
 var util = require('./util');
7 7
 
8  
-function Connection (optionArgs) {
  8
+/**
  9
+ * Create a new connection to the APN service.
  10
+ * @constructor
  11
+ * @param {Object} [options]
  12
+ * @config {String} [cert="cert.pem"] The filename of the connection certificate to load from disk
  13
+ * @config {Buffer|String} [certData] The certificate data. If supplied, will be used instead of loading from disk.
  14
+ * @config {String} [key="key.pem"] The filename of the connection key to load from disk
  15
+ * @config {Buffer|String} [keyData] The key data. If supplied will be used instead of loading from disk.
  16
+ * @config {String} [passphrase] The passphrase for the connection key, if required
  17
+ * @config {Buffer[]|String[]} [ca] An array of strings or Buffers of trusted certificates. If this is omitted several well known "root" CAs will be used, like VeriSign. - You may need to use this as some environments don't include the CA used by Apple
  18
+ * @config {String} [gateway="gateway.push.apple.com"] The gateway server to connect to.
  19
+ * @config {Number} [port=2195] Gateway port
  20
+ * @config {Boolean} [enhanced=true] Whether to use the enhanced notification format (recommended)
  21
+ * @config {Function} [errorCallback] A callback which accepts 2 parameters (err, notification). Recommended when using enhanced format.
  22
+ * @config {Number} [cacheLength] Number of notifications to cache for error purposes (See Readme)
  23
+ */
  24
+function Connection (options) {
9 25
 
10 26
 	this.options = {
11  
-		cert: 'cert.pem' /* Certificate file */,
  27
+		cert: 'cert.pem',
12 28
 		certData: null,
13  
-		key: 'key.pem'	/* Key file */,
  29
+		key: 'key.pem',
14 30
 		keyData: null,
15 31
 		passphrase: null,
16 32
 		ca: null,
17  
-		gateway: 'gateway.push.apple.com' /* gateway address */,
18  
-		port: 2195 /* gateway port */,
19  
-		enhanced: true /* enable enhanced format */,
20  
-		errorCallback: undefined /* Callback when error occurs */,
21  
-		cacheLength: 100 /* Number of notifications to cache for error purposes */
  33
+		gateway: 'gateway.push.apple.com',
  34
+		port: 2195,
  35
+		enhanced: true,
  36
+		errorCallback: undefined,
  37
+		cacheLength: 100
22 38
 	};
23 39
 	
24  
-	util.extend(this.options, optionArgs);
  40
+	util.extend(this.options, options);
25 41
 	
26 42
 	this.certData = null;
27 43
 	this.keyData  = null;
@@ -36,12 +52,19 @@ function Connection (optionArgs) {
36 52
 	this.connectionTimeout = null;
37 53
 };
38 54
 
  55
+/**
  56
+ * @private
  57
+ */
39 58
 Connection.prototype.checkInitialized = function () {
40 59
 	if (this.keyData && this.certData) {
41 60
 		this.deferredInitialize.resolve();
42 61
 	}
43 62
 };
44 63
 
  64
+/**
  65
+ * You should never need to call this method, initialisation and connection is handled by {@link Connection#sendNotification}
  66
+ * @private
  67
+ */
45 68
 Connection.prototype.initialize = function () {
46 69
 	if (this.deferredInitialize) {
47 70
 		return this.deferredInitialize.promise;
@@ -81,6 +104,10 @@ Connection.prototype.initialize = function () {
81 104
 	return this.deferredInitialize.promise;
82 105
 };
83 106
 
  107
+/**
  108
+ * You should never need to call this method, initialisation and connection is handled by {@link Connection#sendNotification}
  109
+ * @private
  110
+ */
84 111
 Connection.prototype.connect = function () {
85 112
 	if (this.deferredConnection) {
86 113
 		return this.deferredConnection.promise;
@@ -129,6 +156,9 @@ Connection.prototype.connect = function () {
129 156
 	return this.deferredConnection.promise;
130 157
 };
131 158
 
  159
+/**
  160
+ * @private
  161
+ */
132 162
 Connection.prototype.socketDrained = function() {
133 163
 	if (this.socket && (this.socket.socket.bufferSize != 0 || !this.socket.writable)) {
134 164
 		return;
@@ -138,12 +168,18 @@ Connection.prototype.socketDrained = function() {
138 168
 	}
139 169
 };
140 170
 
  171
+/**
  172
+ * @private
  173
+ */
141 174
 Connection.prototype.destroyConnection = function() {
142 175
 	if (this.socket) {
143 176
 		this.socket.destroySoon();
144 177
 	}
145 178
 };
146 179
 
  180
+/**
  181
+ * @private
  182
+ */
147 183
 Connection.prototype.restartConnection = function() {
148 184
 	if (this.socket) {
149 185
 		this.socket.removeAllListeners();
@@ -165,17 +201,26 @@ Connection.prototype.restartConnection = function() {
165 201
 	}
166 202
 };
167 203
 
  204
+/**
  205
+ * @private
  206
+ */
168 207
 Connection.prototype.bufferNotification = function (notification) {
169 208
 	this.notificationBuffer.push(notification);
170 209
 };
171 210
 
  211
+/**
  212
+ * @private
  213
+ */
172 214
 Connection.prototype.cacheNotification = function (notification) {
173 215
 	this.cachedNotifications.push(notification);
174 216
 	if (this.cachedNotifications.length > this.options.cacheLength) {
175  
-		this.cachedNotifications.shift().deferred.resolve();
  217
+		this.cachedNotifications.shift();
176 218
 	}
177 219
 };
178 220
 
  221
+/**
  222
+ * @private
  223
+ */
179 224
 Connection.prototype.handleTransmissionError = function (data) {
180 225
 	if (data[0] == 8) {
181 226
 		if (!this.options.enhanced) {
@@ -183,13 +228,13 @@ Connection.prototype.handleTransmissionError = function (data) {
183 228
 		}
184 229
 		
185 230
 		var errorCode = data[1];
186  
-		var identifier = data.readUInt32(2);
  231
+		var identifier = data.readUInt32BE(2);
187 232
 		var notification = undefined;
188 233
 		var foundNotification = false;
189 234
 		var temporaryCache = [];
190 235
 		
191 236
 		while (this.cachedNotifications.length) {
192  
-			notification = cachedNotifications.shift();
  237
+			notification = this.cachedNotifications.shift();
193 238
 			if (notification['_uid'] == identifier) {
194 239
 				foundNotification = true;
195 240
 				break;
@@ -200,12 +245,13 @@ Connection.prototype.handleTransmissionError = function (data) {
200 245
 		// If we haven't found a notification that caused the error then all the notifications must be resent. We should also raise a warning that the cache length isn't sufficient.
201 246
 		if (foundNotification) {
202 247
 			while (temporaryCache.length) {
203  
-				temporaryCache.shift().promise.resolve();
  248
+				temporaryCache.shift();
204 249
 			}
205 250
 			this.raiseError(errorCode, notification);
206 251
 		}
207 252
 		else {
208 253
 			this.cachedNotifications = temporaryCache;
  254
+			this.raiseError(Errors["none"], null);
209 255
 		}
210 256
 		
211 257
 		var count = this.cachedNotifications.length;
@@ -218,34 +264,33 @@ Connection.prototype.handleTransmissionError = function (data) {
218 264
 	}
219 265
 };
220 266
 
  267
+/**
  268
+ * @private
  269
+ */
221 270
 Connection.prototype.raiseError = function(errorCode, notification) {
222  
-	notification.errorCode = errorCode;
223 271
 	if (typeof this.options.errorCallback == 'function') {
224 272
 		this.options.errorCallback(errorCode, notification);
225 273
 	}
226  
-	notification.deferred.reject(notification);
227 274
 };
228 275
 
  276
+/**
  277
+ * Send a notification to the APN service
  278
+ * @param {Notification} notification The notification object to be sent
  279
+ */
229 280
 Connection.prototype.sendNotification = function (notification) {
230  
-	if (!notification.deferred) {
231  
-		notification.deferred = q.defer();
232  
-	}
233  
-	
234 281
 	this.connect().then(function() {
235 282
 		if (this.socket.socket.bufferSize !== 0 || !this.socket.writable) {
236 283
 			this.bufferNotification(notification);
237 284
 			return;
238 285
 		}
239  
-		var encoding = 'utf8';
  286
+		
240 287
 		var token = notification.device.token;
  288
+		
  289
+		var encoding = notification.encoding || 'utf8';
241 290
 		var message = JSON.stringify(notification);
242 291
 		var messageLength = Buffer.byteLength(message, encoding);
243 292
 		var position = 0;
244 293
 		var data;
245  
-		
246  
-		if (notification.encoding) {
247  
-			encoding = notification.encoding;
248  
-		}
249 294
 	
250 295
 		if (token === undefined) {
251 296
 			this.raiseError(Errors['missingDeviceToken'], notification);
@@ -279,26 +324,25 @@ Connection.prototype.sendNotification = function (notification) {
279 324
 		}
280 325
 		else {
281 326
 			data = new Buffer(1 + 2 + token.length + 2 + messageLength);
  327
+			//Command
282 328
 			data[position] = 0;
283 329
 			position++;
284 330
 		}
  331
+		// Token Length
285 332
 		data.writeUInt16BE(token.length, position);
286 333
 		position += 2;
  334
+		// Device Token
287 335
 		position += token.copy(data, position, 0);
  336
+		// Payload Length
288 337
 		data.writeUInt16BE(messageLength, position);
289 338
 		position += 2;
  339
+		//Payload
290 340
 		position += data.write(message, position, encoding);
291 341
 	
292 342
 		this.socket.write(data);
293  
-		
294  
-		if (!this.options.enhanced) {
295  
-			notification.deferred.resolve();
296  
-		}
297 343
 	}.bind(this)).fail(function (error) {
298  
-		notification.deferred.reject(error);
  344
+		this.raiseError(error, notification);
299 345
 	});
300  
-	
301  
-	return notification.deferred.promise;
302 346
 };
303 347
 
304 348
 module.exports = Connection;
59  lib/device.js
... ...
@@ -1,58 +1,23 @@
1 1
 /**
2  
- * Device - Initialise a Device object.
  2
+ * Creates a Device.
3 3
  * @constructor
4 4
  * @param {String|Buffer} token Device token
5  
- * @param {Boolean} [ascii=true] Whether the supplied Device token is in ASCII Format.
6 5
  */
7  
-function Device(/* deviceToken, ascii=true */) {
8  
-	var self = this;
9  
-	self.token = undefined;
10  
-
11  
-	if (arguments.length > 0) {
12  
-		self.setToken.apply(self, arguments);
  6
+function Device(deviceToken) {
  7
+	if(typeof deviceToken == "string") {
  8
+		this.token = new Buffer(deviceToken.replace(/\s/g, ""), "hex");
  9
+	}
  10
+	else {
  11
+		this.token = deviceToken;
13 12
 	}
14 13
 };
15 14
 
16 15
 /**
17  
- * parseToken - Parse an ASCII token into a Buffer
18  
- * @param {String} token Device token
19  
- * @returns {Buffer} Buffer containing the binary representation of the token.
  16
+ * @returns {String} Device token in hex string representation
  17
+ * @since v1.2.0
20 18
  */
21  
-Device.prototype.parseToken = function (token) {
22  
-	token = token.replace(/\s/g, "");
23  
-	var length = Math.ceil(token.length / 2);
24  
-	var hexToken = new Buffer(length);
25  
-	for (var i = 0; i < token.length; i += 2) {
26  
-		var word = token[i];
27  
-		if ((i + 1) >= token.length || typeof(token[i + 1]) === undefined) {
28  
-			word += '0';
29  
-		}
30  
-		else {
31  
-			word += token[i + 1];
32  
-		}
33  
-		hexToken[i / 2] = parseInt(word, 16);
34  
-	}
35  
-	return hexToken;
36  
-};
37  
-
38  
-Device.prototype.setToken = function (newToken, ascii) {
39  
-	if (ascii === undefined || ascii == true) {
40  
-		newToken = this.parseToken(newToken);
41  
-	}
42  
-	this.token = newToken;
43  
-	return this;
44  
-};
45  
-	
46  
-Device.prototype.hexToken = function () {
47  
-	var out = [];
48  
-	var len = this.token.length;
49  
-	var n;
50  
-	for (var i = 0; i < len; i++) {
51  
-		n = this.token[i];
52  
-		if (n < 16) out[i] = "0" + n.toString(16);
53  
-		else out[i] = n.toString(16);
54  
-	}
55  
-	return out.join("");
56  
-};
  19
+Device.prototype.toString = function() {
  20
+	return this.token.toString("hex");
  21
+}
57 22
 
58 23
 module.exports = Device;
5  lib/errors.js
... ...
@@ -1,3 +1,8 @@
  1
+/**
  2
+ * Error codes used by Apple
  3
+ * @see <a href="https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW4">The Binary Interface and Notification Formats</a>
  4
+ */
  5
+
1 6
 var Errors = {
2 7
 	'noErrorsEncountered': 0,
3 8
 	'processingError': 1,
60  lib/feedback.js
... ...
@@ -1,3 +1,4 @@
  1
+var Device = require('./device');
1 2
 var Errors = require('./errors');
2 3
 
3 4
 var fs   = require('fs');
@@ -5,7 +6,23 @@ var q    = require('q');
5 6
 var tls  = require('tls');
6 7
 var util = require('./util');
7 8
 
8  
-function Feedback(optionArgs) {
  9
+/**
  10
+ * Create a new connection to the APN Feedback.
  11
+ * @constructor
  12
+ * @param {Object} [options]
  13
+ * @config {String} [cert="cert.pem"] The filename of the connection certificate to load from disk
  14
+ * @config {Buffer|String} [certData] The certificate data. If supplied, will be used instead of loading from disk.
  15
+ * @config {String} [key="key.pem"] The filename of the connection key to load from disk
  16
+ * @config {Buffer|String} [keyData] The key data. If supplied will be used instead of loading from disk.
  17
+ * @config {String} [passphrase] The passphrase for the connection key, if required
  18
+ * @config {Buffer[]|String[]} [ca] An array of strings or Buffers of trusted certificates. If this is omitted several well known "root" CAs will be used, like VeriSign. - You may need to use this as some environments don't include the CA used by Apple
  19
+ * @config {String} [address="feedback.push.apple.com"] The feedback server to connect to.
  20
+ * @config {Number} [port=2195] Feedback server port
  21
+ * @config {Function} [feedback] A callback which accepts 2 parameters (timestamp, {@link Device}). See: {@link <a href="https://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW3">Communicating with APS</a>.
  22
+ * @config {Function} [errorCallback] Callback which will capture connection errors
  23
+ * @config {Number} [interval=3600] Interval to automatically connect to the Feedback service.
  24
+ */
  25
+function Feedback(options) {
9 26
 
10 27
 	this.options = {
11 28
 		cert: 'cert.pem',					/* Certificate file */
@@ -13,6 +30,7 @@ function Feedback(optionArgs) {
13 30
 		key: 'key.pem',						/* Key file */
14 31
 		keyData: null,						/* Key data */
15 32
 		passphrase: null,                   /* Passphrase for key */
  33
+		ca: null,							/* Certificate Authority
16 34
 		address: 'feedback.push.apple.com',	/* feedback address */
17 35
 		port: 2196,							/* feedback port */
18 36
 		feedback: false,					/* enable feedback service, set to callback */
@@ -20,7 +38,7 @@ function Feedback(optionArgs) {
20 38
 		interval: 3600,						/* interval in seconds to connect to feedback service */
21 39
 	};
22 40
 	
23  
-	util.extend(this.options, optionArgs);
  41
+	util.extend(this.options, options);
24 42
 	
25 43
 	this.certData = null;
26 44
 	this.keyData = null;
@@ -38,12 +56,18 @@ function Feedback(optionArgs) {
38 56
 	this.start();
39 57
 };
40 58
 
  59
+/**
  60
+ * @private
  61
+ */
41 62
 Feedback.prototype.checkInitialized = function () {
42 63
 	if (this.keyData && this.certData) {
43 64
 		this.deferredInitialize.resolve();
44 65
 	}
45 66
 };
46 67
 
  68
+/**
  69
+ * @private
  70
+ */
47 71
 Feedback.prototype.initialize = function () {
48 72
 	if (this.deferredInitialize) {
49 73
 		return this.deferredInitialize.promise;
@@ -82,6 +106,10 @@ Feedback.prototype.initialize = function () {
82 106
 	return this.deferredInitialize.promise;
83 107
 };
84 108
 
  109
+/**
  110
+ * You should call {@link Feedback#start} instead of this method
  111
+ * @private
  112
+ */
85 113
 Feedback.prototype.connect = function () {
86 114
 	if(this.deferredConnection) {
87 115
 		return this.deferredConnection.promise;
@@ -120,6 +148,9 @@ Feedback.prototype.connect = function () {
120 148
 	return this.deferredConnection.promise;
121 149
 };
122 150
 
  151
+/**
  152
+ * @private
  153
+ */
123 154
 Feedback.prototype.receive = function (data) {
124 155
 	var time = 0;
125 156
 	var tokenLength = 0;
@@ -139,36 +170,46 @@ Feedback.prototype.receive = function (data) {
139 170
 		this.readBuffer.copy(token, 0, 6, 6 + tokenLength);
140 171
 		
141 172
 		if (typeof this.options.feedback == 'function') {
142  
-			this.options.feedback(time, token);
  173
+			this.options.feedback(time, new Device(token));
143 174
 		}
144 175
 		this.readBuffer = this.readBuffer.slice(6 + tokenLength);
145 176
 	}
146 177
 }
147 178
 
  179
+/**
  180
+ * @private
  181
+ */
148 182
 Feedback.prototype.destroyConnection = function () {
149 183
 	if (this.socket) {
150 184
 		this.socket.destroySoon();
151 185
 	}
152 186
 };
153 187
 
  188
+/**
  189
+ * @private
  190
+ */
154 191
 Feedback.prototype.resetConnection = function () {
155 192
 	if (this.socket) {
156 193
 		this.socket.removeAllListeners();
157 194
 	}
158 195
 	this.socket = null;
159 196
 	this.deferredConnection = null;
160  
-}
  197
+};
161 198
 
  199
+/**
  200
+ * Connect to the feedback service, also initialise the timer if an interval is specified.
  201
+ */
162 202
 Feedback.prototype.start = function () {
163 203
 	this.cancel();
164 204
 	if (this.options.interval > 0) {
165 205
 		this.interval = setInterval(this.request.bind(this), this.options.interval * 1000);
166 206
 	}
167  
-	else {
168  
-		this.request();
169  
-	}
170  
-}
  207
+	this.request();
  208
+};
171 209
 
  210
+/**
  211
+ * @private
  212
+ */
172 213
 Feedback.prototype.request = function () {
173 214
 	this.connect().fail(function (error) {
174 215
 		if(typeof this.options.errorCallback == "function") {
@@ -177,6 +218,9 @@ Feedback.prototype.request = function () {
177 218
 	}.bind(this));
178 219
 };
179 220
 
  221
+/**
  222
+ * Cancel the timer to stop the Feedback service periodically connecting.
  223
+ */
180 224
 Feedback.prototype.cancel = function () {
181 225
 	if (this.interval !== undefined) {
182 226
 		clearInterval(this.interval);
163  lib/notification.js
... ...
@@ -1,4 +1,10 @@
  1
+/**
  2
+ * Create a notification
  3
+ * @constructor
  4
+ */
1 5
 function Notification () {
  6
+	this.encoding = 'utf8';
  7
+
2 8
 	this.payload = {};
3 9
 	this.expiry = 0;
4 10
 	this.identifier = 0;
@@ -7,8 +13,162 @@ function Notification () {
7 13
 	this.alert = undefined;
8 14
 	this.badge = undefined;
9 15
 	this.sound = undefined;
  16
+	/** @since v1.2.0 */
  17
+	this.newsstandAvailable = undefined;
10 18
 };
11 19
 
  20
+/**
  21
+ * Clone a notification to send to multiple devices
  22
+ * @param {Device} [device] Device the notification will be sent to
  23
+ * @returns {Notification} A notification containing the same properties as the receiver
  24
+ * @since v1.2.0
  25
+ */
  26
+Notification.prototype.clone = function (device) {
  27
+	var notification = new Notification();
  28
+	
  29
+	notification.encoding = this.encoding;
  30
+	notification.payload = this.payload;
  31
+	notification.expiry = this.expiry;
  32
+	notification.identifier = this.identifier;
  33
+	notification.device = device;
  34
+
  35
+	notification.alert = this.alert;
  36
+	notification.badget = this.badge;
  37
+	notification.sound = this.sound;
  38
+	notification.newsstandAvailable = this.newsstandAvailable;
  39
+	
  40
+	return notification
  41
+}
  42
+
  43
+/**
  44
+ * Set the alert text for the notification
  45
+ * @param {String} alertText The text of the alert message.
  46
+ * @see The <a href="https://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1">Payload Documentation</a>
  47
+ * @since v1.2.0
  48
+ */
  49
+Notification.prototype.setAlertText = function (text) {
  50
+	if(typeof this.alert != "object") {
  51
+		this.alert = text;
  52
+	}
  53
+	else {
  54
+		this.prepareAlert();
  55
+		this.alert['body'] = text;
  56
+	}
  57
+}
  58
+
  59
+/**
  60
+ * Set the action-loc-key property on the alert object
  61
+ * @param {String} [key] If a string is specified, displays an alert with two buttons, whose behavior is described in Table 3-1. However, iOS uses the string as a key to get a localized string in the current localization to use for the right button’s title instead of “View”. If the value is null, the system displays an alert with a single OK button that simply dismisses the alert when tapped.
  62
+ * @see The <a href="https://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1">Payload Documentation</a>
  63
+ * @since v1.2.0
  64
+ */
  65
+Notification.prototype.setActionLocKey = function (key) {
  66
+	this.prepareAlert();
  67
+	this.alert['action-loc-key'] = key;
  68
+}
  69
+
  70
+/**
  71
+ * Set the loc-key parameter on the alert object
  72
+ * @param {String} [key] A key to an alert-message string in a Localizable.strings file for the current localization (which is set by the user’s language preference).
  73
+ * @see The <a href="https://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1">Payload Documentation</a>
  74
+ * @since v1.2.0
  75
+ */
  76
+Notification.prototype.setLocKey = function (key) {
  77
+	this.prepareAlert();
  78
+	if(!key) {
  79
+		delete this.alert["loc-key"];
  80
+		return
  81
+	}
  82
+	this.alert['loc-key'] = key;
  83
+}
  84
+
  85
+/**
  86
+ * Set the loc-args parameter on the alert object
  87
+ * @param {String[]} [args] Variable string values to appear in place of the format specifiers in loc-key.
  88
+ * @see The <a href="https://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1">Payload Documentation</a>
  89
+ * @since v1.2.0
  90
+ */
  91
+Notification.prototype.setLocArgs = function (args) {
  92
+	this.prepareAlert();
  93
+	if(!args) {
  94
+		delete this.alert["loc-args"];
  95
+		return
  96
+	}
  97
+	this.alert['loc-args'] = args;
  98
+}
  99
+
  100
+/**
  101
+ * Set the launch-image parameter on the alert object
  102
+ * @param {String} [image] The filename of an image file in the application bundle; it may include the extension or omit it.
  103
+ * @see The <a href="https://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1">Payload Documentation</a>
  104
+ * @since v1.2.0
  105
+ */
  106
+Notification.prototype.setLaunchImage = function (image) {
  107
+	this.prepareAlert();
  108
+	if(!image) {
  109
+		delete this.alert["launch-image"];
  110
+		return
  111
+	}
  112
+	this.alert["launch-image"] = image;
  113
+}
  114
+
  115
+
  116
+/**
  117
+ * If an alert object doesn't already exist create it and transfer any existing message into the .body property
  118
+ * @private
  119
+ * @since v1.2.0
  120
+ */
  121
+Notification.prototype.prepareAlert = function () {
  122
+	var existingValue = this.alert;
  123
+	if(typeof existingValue != "object") {
  124
+		this.alert = {};
  125
+		if(typeof existingValue == "string") {
  126
+			this.alert.body = existingValue;
  127
+		}
  128
+	}
  129
+}
  130
+
  131
+/**
  132
+ * @returns {Number} Byte length of the notification payload
  133
+ * @private
  134
+ * @since v1.2.0
  135
+ */
  136
+Notification.prototype.length = function () {
  137
+	return Buffer.byteLength(JSON.stringify(this), this.encoding || 'utf8');
  138
+}
  139
+
  140
+/**
  141
+ * If the notification payload is too long to send this method will attempt to trim the alert body text.
  142
+ * @returns {Number} The number of characters removed from the body text. If a negative value is returned, the text is too short to be trimmed enough.
  143
+ * @since v1.2.0
  144
+ */
  145
+Notification.prototype.trim = function() {
  146
+	var tooLong = this.length() - 255;
  147
+	if(tooLong <= 0) {
  148
+		return 0;
  149
+	}
  150
+	if(typeof this.alert == "string") {
  151
+		var length = Buffer.byteLength(this.alert.length, this.encoding || 'utf8');
  152
+		if (length < tooLong) {
  153
+			return length - tooLong;
  154
+		}
  155
+		this.alert = this.alert.substring(0, length - tooLong);
  156
+		return tooLong;
  157
+	}
  158
+	else if(typeof this.alert == "object" && typeof this.alert.body == "string") {
  159
+		var length = Buffer.byteLength(this.alert.body.length, this.encoding || 'utf8');
  160
+		if (length < tooLong) {
  161
+			return length - tooLong;
  162
+		}
  163
+		this.alert.body = this.alert.body.substring(0, length - tooLong);
  164
+		return tooLong;
  165
+	}
  166
+	return -tooLong;
  167
+}
  168
+
  169
+/**
  170
+ * @private
  171
+ */
12 172
 Notification.prototype.toJSON = function () {
13 173
 	if (this.payload === undefined) {
14 174
 		this.payload = {};
@@ -25,6 +185,9 @@ Notification.prototype.toJSON = function () {
25 185
 	if (typeof this.alert == 'string' || typeof this.alert == 'object') {
26 186
 		this.payload.aps.alert = this.alert;
27 187
 	}
  188
+	if (this.newsstandAvailable) {
  189
+		this.payload.aps['content-available'] = 1;
  190
+	}
28 191
 	return this.payload;
29 192
 };
30 193
 
2  package.json
... ...
@@ -1,7 +1,7 @@
1 1
 {
2 2
   "name": "apn",
3 3
   "description": "An interface to the Apple Push Notification service for Node.js",
4  
-  "version": "1.2",
  4
+  "version": "1.2.0",
5 5
   "author": "Andrew Naylor <argon@mkbot.net>",
6 6
   "contributors": [ 
7 7
     { "name": "Andrew Naylor", "email": "argon@mkbot.net" }

0 notes on commit 5530d7d

Please sign in to comment.
Something went wrong with that request. Please try again.