node is set to true node is set to true Coverage

Coverage

96%
383
368
15

/Users/thierryschellenbach/workspace/stream-js/src/getstream.js

100%
19
19
0
LineHitsSource
1/**
2 * @module stream
3 * @author Thierry Schellenbach
4 * BSD License
5 */
61var StreamClient = require('./lib/client');
71var errors = require('./lib/errors');
81var request = require('request');
9
101function connect(apiKey, apiSecret, appId, options) {
11 /**
12 * Create StreamClient
13 * @method connect
14 * @param {string} apiKey API key
15 * @param {string} [apiSecret] API secret (only use this on the server)
16 * @param {string} [appId] Application identifier
17 * @param {object} [options] Additional options
18 * @param {string} [options.location] Datacenter location
19 * @return {StreamClient} StreamClient
20 * @example <caption>Basic usage</caption>
21 * stream.connect(apiKey, apiSecret);
22 * @example <caption>or if you want to be able to subscribe and listen</caption>
23 * stream.connect(apiKey, apiSecret, appId);
24 * @example <caption>or on Heroku</caption>
25 * stream.connect(streamURL);
26 * @example <caption>where streamURL looks like</caption>
27 * "https://thierry:pass@gestream.io/?app=1"
28 */
29126 if (typeof (process) !== 'undefined' && process.env.STREAM_URL && !apiKey) {
303 var parts = /https\:\/\/(\w+)\:(\w+)\@([\w-]*).*\?app_id=(\d+)/.exec(process.env.STREAM_URL);
313 apiKey = parts[1];
323 apiSecret = parts[2];
333 var location = parts[3];
343 appId = parts[4];
353 if (options === undefined) {
363 options = {};
37 }
38
393 if (location !== 'getstream') {
401 options.location = location;
41 }
42 }
43
44126 return new StreamClient(apiKey, apiSecret, appId, options);
45}
46
471module.exports.connect = connect;
481module.exports.errors = errors;
491module.exports.request = request;
501module.exports.Client = StreamClient;
51

/Users/thierryschellenbach/workspace/stream-js/src/lib/batch_operations.js

100%
25
25
0
LineHitsSource
11var httpSignature = require('http-signature');
21var request = require('request');
31var errors = require('./errors');
41var Promise = require('./promise');
5
61module.exports = {
7 addToMany: function(activity, feeds, callback) {
8 /**
9 * Add one activity to many feeds
10 * @method addToMany
11 * @memberof StreamClient.prototype
12 * @since 2.3.0
13 * @param {object} activity The activity to add
14 * @param {Array} feeds Array of objects describing the feeds to add to
15 * @param {requestCallback} callback Callback called on completion
16 * @return {Promise} Promise object
17 */
183 return this.makeSignedRequest({
19 url: 'feed/add_to_many/',
20 body: {
21 activity: activity,
22 feeds: feeds,
23 },
24 }, callback);
25 },
26
27 followMany: function(follows, callbackOrActivityCopyLimit, callback) {
28 /**
29 * Follow multiple feeds with one API call
30 * @method followMany
31 * @memberof StreamClient.prototype
32 * @since 2.3.0
33 * @param {Array} follows The follow relations to create
34 * @param {number} [activityCopyLimit] How many activities should be copied from the target feed
35 * @param {requestCallback} [callback] Callback called on completion
36 * @return {Promise} Promise object
37 */
384 var activityCopyLimit, qs = {};
39
404 if (callbackOrActivityCopyLimit && typeof callbackOrActivityCopyLimit === 'number') {
412 activityCopyLimit = callbackOrActivityCopyLimit;
42 }
43
444 if (callbackOrActivityCopyLimit && typeof callbackOrActivityCopyLimit === 'function') {
451 callback = callbackOrActivityCopyLimit;
46 }
47
484 if (activityCopyLimit) {
492 qs['activity_copy_limit'] = activityCopyLimit;
50 }
51
524 return this.makeSignedRequest({
53 url: 'follow_many/',
54 body: follows,
55 qs: qs,
56 }, callback);
57 },
58
59 makeSignedRequest: function(kwargs, cb) {
60 /**
61 * Method to create request to api with application level authentication
62 * @method makeSignedRequest
63 * @memberof StreamClient.prototype
64 * @since 2.3.0
65 * @access private
66 * @param {object} kwargs Arguments for the request
67 * @param {requestCallback} cb Callback to call on completion
68 * @return {Promise} Promise object
69 */
709 if (!this.apiSecret) {
711 throw new errors.SiteError('Missing secret, which is needed to perform signed requests, use var client = stream.connect(key, secret);');
72 }
73
748 return new Promise(function(fulfill, reject) {
758 this.send('request', 'post', kwargs, cb);
76
778 kwargs.url = this.enrichUrl(kwargs.url);
788 kwargs.json = true;
798 kwargs.method = 'POST';
808 kwargs.headers = { 'X-Api-Key': this.apiKey };
81
828 var callback = this.wrapPromiseTask(cb, fulfill, reject);
838 var req = request(kwargs, callback);
84
858 httpSignature.sign(req, {
86 algorithm: 'hmac-sha256',
87 key: this.apiSecret,
88 keyId: this.apiKey,
89 });
90 }.bind(this));
91 },
92};
93

/Users/thierryschellenbach/workspace/stream-js/src/lib/client.js

95%
166
159
7
LineHitsSource
11var request = require('request');
21var StreamFeed = require('./feed');
31var signing = require('./signing');
41var errors = require('./errors');
51var utils = require('./utils');
61var BatchOperations = require('./batch_operations');
71var Promise = require('./promise');
81var qs = require('qs');
91var url = require('url');
101var Faye = require('faye');
11
12/**
13 * @callback requestCallback
14 * @param {object} [errors]
15 * @param {object} response
16 * @param {object} body
17 */
18
191var StreamClient = function() {
20 /**
21 * Client to connect to Stream api
22 * @class StreamClient
23 */
24126 this.initialize.apply(this, arguments);
25};
26
271StreamClient.prototype = {
28 baseUrl: 'https://api.getstream.io/api/',
29 baseAnalyticsUrl: 'https://analytics.getstream.io/analytics/',
30
31 initialize: function(apiKey, apiSecret, appId, options) {
32 /**
33 * Initialize a client
34 * @method intialize
35 * @memberof StreamClient.prototype
36 * @param {string} apiKey - the api key
37 * @param {string} [apiSecret] - the api secret
38 * @param {string} [appId] - id of the app
39 * @param {object} [options] - additional options
40 * @param {string} [options.location] - which data center to use
41 * @param {boolean} [options.expireTokens=false] - whether to use a JWT timestamp field (i.e. iat)
42 * @example <caption>initialize is not directly called by via stream.connect, ie:</caption>
43 * stream.connect(apiKey, apiSecret)
44 * @example <caption>secret is optional and only used in server side mode</caption>
45 * stream.connect(apiKey, null, appId);
46 */
47126 this.apiKey = apiKey;
48126 this.apiSecret = apiSecret;
49126 this.appId = appId;
50126 this.options = options || {};
51126 this.version = this.options.version || 'v1.0';
52126 this.fayeUrl = this.options.fayeUrl || 'https://faye.getstream.io/faye';
53126 this.fayeClient = null;
54 // track a source name for the api calls, ie get started or databrowser
55126 this.group = this.options.group || 'unspecified';
56 // track subscriptions made on feeds created by this client
57126 this.subscriptions = {};
58126 this.expireTokens = this.options.expireTokens ? this.options.expireTokens : false;
59 // which data center to use
60126 this.location = this.options.location;
61126 if (this.location) {
6254 this.baseUrl = 'https://' + this.location + '-api.getstream.io/api/';
63 }
64
65126 if (typeof (process) !== 'undefined' && process.env.LOCAL) {
660 this.baseUrl = 'http://localhost:8000/api/';
67 }
68
69126 if (typeof (process) !== 'undefined' && process.env.LOCAL_FAYE) {
700 this.fayeUrl = 'http://localhost:9999/faye/';
71 }
72
73126 if (typeof (process) !== 'undefined' && process.env.STREAM_BASE_URL) {
740 this.baseUrl = process.env.STREAM_BASE_URL;
75 }
76
77126 this.handlers = {};
78126 this.browser = typeof (window) !== 'undefined';
79126 this.node = !this.browser;
80
81126 if (this.browser && this.apiSecret) {
820 throw new errors.FeedError('You are publicly sharing your private key. Dont use the private key while in the browser.');
83 }
84 },
85
86 on: function(event, callback) {
87 /**
88 * Support for global event callbacks
89 * This is useful for generic error and loading handling
90 * @method on
91 * @memberof StreamClient.prototype
92 * @param {string} event - Name of the event
93 * @param {function} callback - Function that is called when the event fires
94 * @example
95 * client.on('request', callback);
96 * client.on('response', callback);
97 */
982 this.handlers[event] = callback;
99 },
100
101 off: function(key) {
102 /**
103 * Remove one or more event handlers
104 * @method off
105 * @memberof StreamClient.prototype
106 * @param {string} [key] - Name of the handler
107 * @example
108 * client.off() removes all handlers
109 * client.off(name) removes the specified handler
110 */
1111 if (key === undefined) {
1121 this.handlers = {};
113 } else {
1140 delete this.handlers[key];
115 }
116 },
117
118 send: function() {
119 /**
120 * Call the given handler with the arguments
121 * @method send
122 * @memberof StreamClient.prototype
123 * @access private
124 */
125148 var args = Array.prototype.slice.call(arguments);
126148 var key = args[0];
127148 args = args.slice(1);
128148 if (this.handlers[key]) {
1292 this.handlers[key].apply(this, args);
130 }
131 },
132
133 wrapPromiseTask: function(cb, fulfill, reject) {
134 /**
135 * Wrap a task to be used as a promise
136 * @method wrapPromiseTask
137 * @memberof StreamClient.prototype
138 * @private
139 * @param {requestCallback} cb
140 * @param {function} fulfill
141 * @param {function} reject
142 * @return {function}
143 */
14474 var client = this;
145
14674 var callback = this.wrapCallback(cb);
14774 return function task(error, response, body) {
14874 if (error) {
1492 reject({
150 error: error,
151 response: response,
152 });
15372 } else if (!/^2/.test('' + response.statusCode)) {
1544 reject({
155 error: body,
156 response: response,
157 });
158 } else {
15968 fulfill(body);
160 }
161
16274 callback.apply(client, arguments);
163 };
164 },
165
166 wrapCallback: function(cb) {
167 /**
168 * Wrap callback for HTTP request
169 * @method wrapCallBack
170 * @memberof StreamClient.prototype
171 * @access private
172 */
17374 var client = this;
174
17574 function callback() {
176 // first hit the global callback, subsequently forward
17774 var args = Array.prototype.slice.call(arguments);
17874 var sendArgs = ['response'].concat(args);
17974 client.send.apply(client, sendArgs);
18074 if (cb !== undefined) {
18155 cb.apply(client, args);
182 }
183 }
184
18574 return callback;
186 },
187
188 userAgent: function() {
189 /**
190 * Get the current user agent
191 * @method userAgent
192 * @memberof StreamClient.prototype
193 * @return {string} current user agent
194 */
19566 var description = (this.node) ? 'node' : 'browser';
196 // TODO: get the version here in a way which works in both and browserify
19766 var version = 'unknown';
19866 return 'stream-javascript-client-' + description + '-' + version;
199 },
200
201 getReadOnlyToken: function(feedSlug, userId) {
202 /**
203 * Returns a token that allows only read operations
204 *
205 * @method getReadOnlyToken
206 * @memberof StreamClient.prototype
207 * @param {string} feedSlug - The feed slug to get a read only token for
208 * @param {string} userId - The user identifier
209 * @return {string} token
210 * @example
211 * client.getReadOnlyToken('user', '1');
212 */
213105 return this.feed(feedSlug, userId).getReadOnlyToken();
214 },
215
216 getReadWriteToken: function(feedSlug, userId) {
217 /**
218 * Returns a token that allows read and write operations
219 *
220 * @method getReadWriteToken
221 * @memberof StreamClient.prototype
222 * @param {string} feedSlug - The feed slug to get a read only token for
223 * @param {string} userId - The user identifier
224 * @return {string} token
225 * @example
226 * client.getReadWriteToken('user', '1');
227 */
2281 return this.feed(feedSlug, userId).getReadWriteToken();
229 },
230
231 feed: function(feedSlug, userId, token, siteId, options) {
232 /**
233 * Returns a feed object for the given feed id and token
234 * @method feed
235 * @memberof StreamClient.prototype
236 * @param {string} feedSlug - The feed slug
237 * @param {string} userId - The user identifier
238 * @param {string} [token] - The token
239 * @param {string} [siteId] - The site identifier
240 * @param {object} [options] - Additional function options
241 * @param {boolean} [options.readOnly] - A boolean indicating whether to generate a read only token for this feed
242 * @return {StreamFeed}
243 * @example
244 * client.feed('user', '1', 'token2');
245 */
246
247549 options = options || {};
248
249549 if (!feedSlug || !userId) {
2501 throw new errors.FeedError('Please provide a feed slug and user id, ie client.feed("user", "1")');
251 }
252
253548 if (feedSlug.indexOf(':') !== -1) {
2541 throw new errors.FeedError('Please initialize the feed using client.feed("user", "1") not client.feed("user:1")');
255 }
256
257547 utils.validateFeedSlug(feedSlug);
258546 utils.validateUserId(userId);
259
260 // raise an error if there is no token
261545 if (!this.apiSecret && !token) {
2620 throw new errors.FeedError('Missing token, in client side mode please provide a feed secret');
263 }
264
265 // create the token in server side mode
266545 if (this.apiSecret && !token) {
267538 var feedId = '' + feedSlug + userId;
268 // use scoped token if read-only access is necessary
269538 token = options.readOnly ? this.getReadOnlyToken(feedSlug, userId) : signing.sign(this.apiSecret, feedId);
270 }
271
272545 var feed = new StreamFeed(this, feedSlug, userId, token, siteId);
273545 return feed;
274 },
275
276 enrichUrl: function(relativeUrl) {
277 /**
278 * Combines the base url with version and the relative url
279 * @method enrichUrl
280 * @memberof StreamClient.prototype
281 * @private
282 * @param {string} relativeUrl
283 */
28474 var url = this.baseUrl + this.version + '/' + relativeUrl;
28574 return url;
286 },
287
288 enrichKwargs: function(kwargs) {
289 /**
290 * Adds the API key and the signature
291 * @method enrichKwargs
292 * @memberof StreamClient.prototype
293 * @param {object} kwargs
294 * @private
295 */
29666 kwargs.url = this.enrichUrl(kwargs.url);
29766 if (kwargs.qs === undefined) {
29838 kwargs.qs = {};
299 }
300
30166 kwargs.qs['api_key'] = this.apiKey;
30266 kwargs.qs.location = this.group;
30366 kwargs.json = true;
30466 var signature = kwargs.signature || this.signature;
30566 kwargs.headers = {};
306
307 // auto-detect authentication type and set HTTP headers accordingly
30866 if (signing.isJWTSignature(signature)) {
30912 kwargs.headers['stream-auth-type'] = 'jwt';
31012 signature = signature.split(' ').reverse()[0];
311 } else {
31254 kwargs.headers['stream-auth-type'] = 'simple';
313 }
314
31566 kwargs.headers.Authorization = signature;
31666 kwargs.headers['X-Stream-Client'] = this.userAgent();
31766 return kwargs;
318 },
319
320 signActivity: function(activity) {
321 /**
322 * We automatically sign the to parameter when in server side mode
323 * @method signActivities
324 * @memberof StreamClient.prototype
325 * @private
326 * @param {object} [activity] Activity to sign
327 */
32821 return this.signActivities([activity])[0];
329 },
330
331 signActivities: function(activities) {
332 /**
333 * We automatically sign the to parameter when in server side mode
334 * @method signActivities
335 * @memberof StreamClient.prototype
336 * @private
337 * @param {array} Activities
338 */
33925 if (!this.apiSecret) {
3400 return activities;
341 }
342
34325 for (var i = 0; i < activities.length; i++) {
34437 var activity = activities[i];
34537 var to = activity.to || [];
34637 var signedTo = [];
34737 for (var j = 0; j < to.length; j++) {
3482 var feedId = to[j];
3492 var feedSlug = feedId.split(':')[0];
3502 var userId = feedId.split(':')[1];
3512 var token = this.feed(feedSlug, userId).token;
3522 var signedFeed = feedId + ' ' + token;
3532 signedTo.push(signedFeed);
354 }
355
35637 activity.to = signedTo;
357 }
358
35925 return activities;
360 },
361
362 getFayeAuthorization: function() {
363 /**
364 * Get the authorization middleware to use Faye with getstream.io
365 * @method getFayeAuthorization
366 * @memberof StreamClient.prototype
367 * @private
368 * @return {object} Faye authorization middleware
369 */
3706 var apiKey = this.apiKey,
371 self = this;
372
3736 return {
374 incoming: function(message, callback) {
37530 callback(message);
376 },
377
378 outgoing: function(message, callback) {
37921 if (message.subscription && self.subscriptions[message.subscription]) {
3808 var subscription = self.subscriptions[message.subscription];
381
3828 message.ext = {
383 'user_id': subscription.userId,
384 'api_key': apiKey,
385 'signature': subscription.token,
386 };
387 }
388
38921 callback(message);
390 },
391 };
392 },
393
394 getFayeClient: function() {
395 /**
396 * Returns this client's current Faye client
397 * @method getFayeClient
398 * @memberof StreamClient.prototype
399 * @private
400 * @return {object} Faye client
401 */
40211 if (this.fayeClient === null) {
4036 this.fayeClient = new Faye.Client(this.fayeUrl);
4046 var authExtension = this.getFayeAuthorization();
4056 this.fayeClient.addExtension(authExtension);
406 }
407
40811 return this.fayeClient;
409 },
410
411 get: function(kwargs, cb) {
412 /**
413 * Shorthand function for get request
414 * @method get
415 * @memberof StreamClient.prototype
416 * @private
417 * @param {object} kwargs
418 * @param {requestCallback} cb Callback to call on completion
419 * @return {Promise} Promise object
420 */
42124 return new Promise(function(fulfill, reject) {
42224 this.send('request', 'get', kwargs, cb);
42324 kwargs = this.enrichKwargs(kwargs);
42424 kwargs.method = 'GET';
42524 var callback = this.wrapPromiseTask(cb, fulfill, reject);
42624 request(kwargs, callback);
427 }.bind(this));
428 },
429
430 post: function(kwargs, cb) {
431 /**
432 * Shorthand function for post request
433 * @method post
434 * @memberof StreamClient.prototype
435 * @private
436 * @param {object} kwargs
437 * @param {requestCallback} cb Callback to call on completion
438 * @return {Promise} Promise object
439 */
44038 return new Promise(function(fulfill, reject) {
44138 this.send('request', 'post', kwargs, cb);
44238 kwargs = this.enrichKwargs(kwargs);
44338 kwargs.method = 'POST';
44438 var callback = this.wrapPromiseTask(cb, fulfill, reject);
44538 request(kwargs, callback);
446 }.bind(this));
447 },
448
449 delete: function(kwargs, cb) {
450 /**
451 * Shorthand function for delete request
452 * @method delete
453 * @memberof StreamClient.prototype
454 * @private
455 * @param {object} kwargs
456 * @param {requestCallback} cb Callback to call on completion
457 * @return {Promise} Promise object
458 */
4594 return new Promise(function(fulfill, reject) {
4604 this.send('request', 'delete', kwargs, cb);
4614 kwargs = this.enrichKwargs(kwargs);
4624 kwargs.method = 'DELETE';
4634 var callback = this.wrapPromiseTask(cb, fulfill, reject);
4644 request(kwargs, callback);
465 }.bind(this));
466 },
467
468 updateActivities: function(activities, callback) {
469 /**
470 * Updates all supplied activities on the getstream-io api
471 * @since 3.1.0
472 * @param {array} activities list of activities to update
473 * @return {Promise}
474 */
4758 if (! (activities instanceof Array)) {
4761 throw new TypeError('The activities argument should be an Array');
477 }
478
4797 var authToken = signing.JWTScopeToken(this.apiSecret, 'activities', '*', { feedId: '*', expireTokens: this.expireTokens });
480
4817 var data = {
482 activities: activities,
483 };
484
4857 return this.post({
486 url: 'activities/',
487 body: data,
488 signature: authToken,
489 }, callback);
490 },
491
492 updateActivity: function(activity) {
493 /**
494 * Updates one activity on the getstream-io api
495 * @since 3.1.0
496 * @param {object} activity The activity to update
497 * @return {Promise}
498 */
4995 return this.updateActivities([activity]);
500 },
501
502};
503
5041if (qs) {
5051 StreamClient.prototype.createRedirectUrl = function(targetUrl, userId, events) {
506 /**
507 * Creates a redirect url for tracking the given events in the context of
508 * an email using Stream's analytics platform. Learn more at
509 * getstream.io/personalization
510 * @method createRedirectUrl
511 * @memberof StreamClient.prototype
512 * @param {string} targetUrl Target url
513 * @param {string} userId User id to track
514 * @param {array} events List of events to track
515 * @return {string} The redirect url
516 */
5173 var uri = url.parse(targetUrl);
518
5193 if (!(uri.host || (uri.hostname && uri.port)) && !uri.isUnix) {
5201 throw new errors.MissingSchemaError('Invalid URI: "' + url.format(uri) + '"');
521 }
522
5232 var authToken = signing.JWTScopeToken(this.apiSecret, 'redirect_and_track', '*', { userId: userId, expireTokens: this.expireTokens });
5242 var analyticsUrl = this.baseAnalyticsUrl + 'redirect/';
5252 var kwargs = {
526 'auth_type': 'jwt',
527 'authorization': authToken,
528 'url': targetUrl,
529 'api_key': this.apiKey,
530 'events': JSON.stringify(events),
531 };
532
5332 var qString = utils.rfc3986(qs.stringify(kwargs, null, null, {}));
534
5352 return analyticsUrl + '?' + qString;
536 };
537}
538
539// If we are in a node environment and batchOperations is available add the methods to the prototype of StreamClient
5401if (BatchOperations) {
5411 for (var key in BatchOperations) {
5423 if (BatchOperations.hasOwnProperty(key)) {
5433 StreamClient.prototype[key] = BatchOperations[key];
544 }
545 }
546}
547
5481module.exports = StreamClient;
549

/Users/thierryschellenbach/workspace/stream-js/src/lib/errors.js

86%
22
19
3
LineHitsSource
11var errors = module.exports;
2
31var canCapture = (typeof Error.captureStackTrace === 'function');
41var canStack = !!(new Error()).stack;
5
6/**
7 * Abstract error object
8 * @class ErrorAbstract
9 * @access private
10 * @param {string} [msg] Error message
11 * @param {function} constructor
12 */
131function ErrorAbstract(msg, constructor) {
1414 this.message = msg;
15
1614 Error.call(this, this.message);
17
1814 if (canCapture) {
1914 Error.captureStackTrace(this, constructor);
200 } else if (canStack) {
210 this.stack = (new Error()).stack;
22 } else {
230 this.stack = '';
24 }
25}
26
271errors._Abstract = ErrorAbstract;
281ErrorAbstract.prototype = new Error();
29
30/**
31 * FeedError
32 * @class FeedError
33 * @access private
34 * @extends ErrorAbstract
35 * @memberof Stream.errors
36 * @param {String} [msg] - An error message that will probably end up in a log.
37 */
381errors.FeedError = function FeedError(msg) {
397 ErrorAbstract.call(this, msg);
40};
41
421errors.FeedError.prototype = new ErrorAbstract();
43
44/**
45 * SiteError
46 * @class SiteError
47 * @access private
48 * @extends ErrorAbstract
49 * @memberof Stream.errors
50 * @param {string} [msg] An error message that will probably end up in a log.
51 */
521errors.SiteError = function SiteError(msg) {
532 ErrorAbstract.call(this, msg);
54};
55
561errors.SiteError.prototype = new ErrorAbstract();
57
58/**
59 * MissingSchemaError
60 * @method MissingSchema
61 * @access private
62 * @extends ErrorAbstract
63 * @memberof Stream.errors
64 * @param {string} msg
65 */
661errors.MissingSchemaError = function MissingSchemaError(msg) {
672 ErrorAbstract.call(this, msg);
68};
69
701errors.MissingSchemaError.prototype = new ErrorAbstract();
71

/Users/thierryschellenbach/workspace/stream-js/src/lib/feed.js

95%
69
66
3
LineHitsSource
11var errors = require('./errors');
21var utils = require('./utils');
31var signing = require('./signing');
4
51var StreamFeed = function() {
6 /**
7 * Manage api calls for specific feeds
8 * The feed object contains convenience functions such add activity, remove activity etc
9 * @class StreamFeed
10 */
11545 this.initialize.apply(this, arguments);
12};
13
141StreamFeed.prototype = {
15 initialize: function(client, feedSlug, userId, token) {
16 /**
17 * Initialize a feed object
18 * @method intialize
19 * @memberof StreamFeed.prototype
20 * @param {StreamClient} client - The stream client this feed is constructed from
21 * @param {string} feedSlug - The feed slug
22 * @param {string} userId - The user id
23 * @param {string} [token] - The authentication token
24 */
25545 this.client = client;
26545 this.slug = feedSlug;
27545 this.userId = userId;
28545 this.id = this.slug + ':' + this.userId;
29545 this.token = token;
30
31545 this.feedUrl = this.id.replace(':', '/');
32545 this.feedTogether = this.id.replace(':', '');
33545 this.signature = this.feedTogether + ' ' + this.token;
34
35 // faye setup
36545 this.notificationChannel = 'site-' + this.client.appId + '-feed-' + this.feedTogether;
37 },
38
39 addActivity: function(activity, callback) {
40 /**
41 * Adds the given activity to the feed and
42 * calls the specified callback
43 * @method addActivity
44 * @memberof StreamFeed.prototype
45 * @param {object} activity - The activity to add
46 * @param {requestCallback} callback - Callback to call on completion
47 * @return {Promise} Promise object
48 */
4921 activity = this.client.signActivity(activity);
50
5121 return this.client.post({
52 url: 'feed/' + this.feedUrl + '/',
53 body: activity,
54 signature: this.signature,
55 }, callback);
56 },
57
58 removeActivity: function(activityId, callback) {
59 /**
60 * Removes the activity by activityId
61 * @method removeActivity
62 * @memberof StreamFeed.prototype
63 * @param {string} activityId Identifier of activity to remove
64 * @param {requestCallback} callback Callback to call on completion
65 * @return {Promise} Promise object
66 * @example
67 * feed.removeActivity(activityId);
68 * @example
69 * feed.removeActivity({'foreign_id': foreignId});
70 */
712 var identifier = (activityId.foreignId) ? activityId.foreignId : activityId;
722 var params = {};
732 if (activityId.foreignId) {
741 params['foreign_id'] = '1';
75 }
76
772 return this.client.delete({
78 url: 'feed/' + this.feedUrl + '/' + identifier + '/',
79 qs: params,
80 signature: this.signature,
81 }, callback);
82 },
83
84 addActivities: function(activities, callback) {
85 /**
86 * Adds the given activities to the feed and calls the specified callback
87 * @method addActivities
88 * @memberof StreamFeed.prototype
89 * @param {Array} activities Array of activities to add
90 * @param {requestCallback} callback Callback to call on completion
91 * @return {Promise} XHR request object
92 */
934 activities = this.client.signActivities(activities);
944 var data = {
95 activities: activities,
96 };
974 var xhr = this.client.post({
98 url: 'feed/' + this.feedUrl + '/',
99 body: data,
100 signature: this.signature,
101 }, callback);
1024 return xhr;
103 },
104
105 follow: function(targetSlug, targetUserId, options, callback) {
106 /**
107 * Follows the given target feed
108 * @method follow
109 * @memberof StreamFeed.prototype
110 * @param {string} targetSlug Slug of the target feed
111 * @param {string} targetUserId User identifier of the target feed
112 * @param {object} options Additional options
113 * @param {number} options.activityCopyLimit Limit the amount of activities copied over on follow
114 * @param {requestCallback} callback Callback to call on completion
115 * @return {Promise} Promise object
116 * @example feed.follow('user', '1');
117 * @example feed.follow('user', '1', callback);
118 * @example feed.follow('user', '1', options, callback);
119 */
1208 utils.validateFeedSlug(targetSlug);
1217 utils.validateUserId(targetUserId);
122
1236 var activityCopyLimit;
1246 var last = arguments[arguments.length - 1];
125 // callback is always the last argument
1266 callback = (last.call) ? last : undefined;
1276 var target = targetSlug + ':' + targetUserId;
128
129 // check for additional options
1306 if (options && !options.call) {
1311 if (typeof options.limit !== "undefined" && options.limit !== null) {
1321 activityCopyLimit = options.limit;
133 }
134 }
135
1366 var body = {
137 target: target,
138 };
139
1406 if (typeof activityCopyLimit !== "undefined" && activityCopyLimit !== null) {
1411 body['activity_copy_limit'] = activityCopyLimit;
142 }
143
1446 return this.client.post({
145 url: 'feed/' + this.feedUrl + '/following/',
146 body: body,
147 signature: this.signature,
148 }, callback);
149 },
150
151 unfollow: function(targetSlug, targetUserId, optionsOrCallback, callback) {
152 /**
153 * Unfollow the given feed
154 * @method unfollow
155 * @memberof StreamFeed.prototype
156 * @param {string} targetSlug Slug of the target feed
157 * @param {string} targetUserId [description]
158 * @param {requestCallback|object} optionsOrCallback
159 * @param {boolean} optionOrCallback.keepHistory when provided the activities from target
160 * feed will not be kept in the feed
161 * @param {requestCallback} callback Callback to call on completion
162 * @return {object} XHR request object
163 * @example feed.unfollow('user', '2', callback);
164 */
1652 var options = {}, qs = {};
1663 if (typeof optionsOrCallback === 'function') callback = optionsOrCallback;
1673 if (typeof optionsOrCallback === 'object') options = optionsOrCallback;
1683 if (typeof options.keepHistory === 'boolean' && options.keepHistory) qs['keep_history'] = '1';
169
1702 utils.validateFeedSlug(targetSlug);
1712 utils.validateUserId(targetUserId);
1722 var targetFeedId = targetSlug + ':' + targetUserId;
1732 var xhr = this.client.delete({
174 url: 'feed/' + this.feedUrl + '/following/' + targetFeedId + '/',
175 qs: qs,
176 signature: this.signature,
177 }, callback);
1782 return xhr;
179 },
180
181 following: function(options, callback) {
182 /**
183 * List which feeds this feed is following
184 * @method following
185 * @memberof StreamFeed.prototype
186 * @param {object} options Additional options
187 * @param {string} options.filter Filter to apply on search operation
188 * @param {requestCallback} callback Callback to call on completion
189 * @return {Promise} Promise object
190 * @example feed.following({limit:10, filter: ['user:1', 'user:2']}, callback);
191 */
1922 if (options !== undefined && options.filter) {
1931 options.filter = options.filter.join(',');
194 }
195
1962 return this.client.get({
197 url: 'feed/' + this.feedUrl + '/following/',
198 qs: options,
199 signature: this.signature,
200 }, callback);
201 },
202
203 followers: function(options, callback) {
204 /**
205 * List the followers of this feed
206 * @method followers
207 * @memberof StreamFeed.prototype
208 * @param {object} options Additional options
209 * @param {string} options.filter Filter to apply on search operation
210 * @param {requestCallback} callback Callback to call on completion
211 * @return {Promise} Promise object
212 * @example
213 * feed.followers({limit:10, filter: ['user:1', 'user:2']}, callback);
214 */
2151 if (options !== undefined && options.filter) {
2160 options.filter = options.filter.join(',');
217 }
218
2191 return this.client.get({
220 url: 'feed/' + this.feedUrl + '/followers/',
221 qs: options,
222 signature: this.signature,
223 }, callback);
224 },
225
226 get: function(options, callback) {
227 /**
228 * Reads the feed
229 * @method get
230 * @memberof StreamFeed.prototype
231 * @param {object} options Additional options
232 * @param {requestCallback} callback Callback to call on completion
233 * @return {Promise} Promise object
234 * @example feed.get({limit: 10, id_lte: 'activity-id'})
235 * @example feed.get({limit: 10, mark_seen: true})
236 */
23721 if (options && options['mark_read'] && options['mark_read'].join) {
2380 options['mark_read'] = options['mark_read'].join(',');
239 }
240
24121 if (options && options['mark_seen'] && options['mark_seen'].join) {
2420 options['mark_seen'] = options['mark_seen'].join(',');
243 }
244
24521 return this.client.get({
246 url: 'feed/' + this.feedUrl + '/',
247 qs: options,
248 signature: this.signature,
249 }, callback);
250 },
251
252 getFayeClient: function() {
253 /**
254 * Returns the current faye client object
255 * @method getFayeClient
256 * @memberof StreamFeed.prototype
257 * @access private
258 * @return {object} Faye client
259 */
26011 return this.client.getFayeClient();
261 },
262
263 subscribe: function(callback) {
264 /**
265 * Subscribes to any changes in the feed, return a promise
266 * @method subscribe
267 * @memberof StreamFeed.prototype
268 * @param {function} callback Callback to call on completion
269 * @return {Promise} Promise object
270 * @example
271 * feed.subscribe(callback).then(function(){
272 * console.log('we are now listening to changes');
273 * });
274 */
2759 if (!this.client.appId) {
2761 throw new errors.SiteError('Missing app id, which is needed to subscribe, use var client = stream.connect(key, secret, appId);');
277 }
278
2798 this.client.subscriptions['/' + this.notificationChannel] = {
280 token: this.token,
281 userId: this.notificationChannel,
282 };
283
2848 return this.getFayeClient().subscribe('/' + this.notificationChannel, callback);
285 },
286
287 getReadOnlyToken: function() {
288 /**
289 * Returns a token that allows only read operations
290 *
291 * @method getReadOnlyToken
292 * @memberof StreamClient.prototype
293 * @param {string} feedSlug - The feed slug to get a read only token for
294 * @param {string} userId - The user identifier
295 * @return {string} token
296 * @example
297 * client.getReadOnlyToken('user', '1');
298 */
299106 var feedId = '' + this.slug + this.userId;
300106 return signing.JWTScopeToken(this.client.apiSecret, '*', 'read', { feedId: feedId, expireTokens: this.client.expireTokens });
301 },
302
303 getReadWriteToken: function() {
304 /**
305 * Returns a token that allows read and write operations
306 *
307 * @method getReadWriteToken
308 * @memberof StreamClient.prototype
309 * @param {string} feedSlug - The feed slug to get a read only token for
310 * @param {string} userId - The user identifier
311 * @return {string} token
312 * @example
313 * client.getReadWriteToken('user', '1');
314 */
3152 var feedId = '' + this.slug + this.userId;
3162 return signing.JWTScopeToken(this.client.apiSecret, '*', '*', { feedId: feedId, expireTokens: this.client.expireTokens });
317 },
318};
319
3201module.exports = StreamFeed;

/Users/thierryschellenbach/workspace/stream-js/src/lib/promise.js

100%
2
2
0
LineHitsSource
11var Promise = require('faye/src/util/promise');
2
31module.exports = Promise;

/Users/thierryschellenbach/workspace/stream-js/src/lib/signing.js

98%
52
51
1
LineHitsSource
11var crypto = require('crypto');
21var jwt = require('jsonwebtoken');
31var JWS_REGEX = /^[a-zA-Z0-9\-_]+?\.[a-zA-Z0-9\-_]+?\.([a-zA-Z0-9\-_]+)?$/;
41var Base64 = require('Base64');
5
61function makeUrlSafe(s) {
7 /*
8 * Makes the given base64 encoded string urlsafe
9 */
10434 var escaped = s.replace(/\+/g, '-').replace(/\//g, '_');
11434 return escaped.replace(/^=+/, '').replace(/=+$/, '');
12}
13
141function decodeBase64Url(base64UrlString) {
1514 try {
1614 return Base64.atob(toBase64(base64UrlString));
17 } catch (e) {
181 if (e.name === 'InvalidCharacterError') {
191 return undefined;
20 } else {
210 throw e;
22 }
23 }
24}
25
261function safeJsonParse(thing) {
2714 if (typeof (thing) === 'object') return thing;
2814 try {
2914 return JSON.parse(thing);
30 } catch (e) {
311 return undefined;
32 }
33}
34
351function padString(string) {
3614 var segmentLength = 4;
3714 var diff = string.length % segmentLength;
3814 if (!diff)
3913 return string;
401 var padLength = segmentLength - diff;
41
421 while (padLength--)
433 string += '=';
441 return string;
45}
46
471function toBase64(base64UrlString) {
4814 var b64str = padString(base64UrlString)
49 .replace(/\-/g, '+')
50 .replace(/_/g, '/');
5114 return b64str;
52}
53
541function headerFromJWS(jwsSig) {
5514 var encodedHeader = jwsSig.split('.', 1)[0];
5614 return safeJsonParse(decodeBase64Url(encodedHeader));
57}
58
591exports.headerFromJWS = headerFromJWS;
60
611exports.sign = function(apiSecret, feedId) {
62 /*
63 * Setup sha1 based on the secret
64 * Get the digest of the value
65 * Base64 encode the result
66 *
67 * Also see
68 * https://github.com/tbarbugli/stream-ruby/blob/master/lib/stream/signer.rb
69 * https://github.com/tschellenbach/stream-python/blob/master/stream/signing.py
70 *
71 * Steps
72 * apiSecret: tfq2sdqpj9g446sbv653x3aqmgn33hsn8uzdc9jpskaw8mj6vsnhzswuwptuj9su
73 * feedId: flat1
74 * digest: Q\xb6\xd5+\x82\xd58\xdeu\x80\xc5\xe3\xb8\xa5bL1\xf1\xa3\xdb
75 * token: UbbVK4LVON51gMXjuKViTDHxo9s
76 */
77434 var hashedSecret = new crypto.createHash('sha1').update(apiSecret).digest();
78434 var hmac = crypto.createHmac('sha1', hashedSecret);
79434 var digest = hmac.update(feedId).digest('base64');
80434 var token = makeUrlSafe(digest);
81434 return token;
82};
83
841exports.JWTScopeToken = function(apiSecret, resource, action, opts) {
85 /**
86 * Creates the JWT token for feedId, resource and action using the apiSecret
87 * @method JWTScopeToken
88 * @memberof signing
89 * @private
90 * @param {string} apiSecret - API Secret key
91 * @param {string} resource - JWT payload resource
92 * @param {string} action - JWT payload action
93 * @param {object} [options] - Optional additional options
94 * @param {string} [options.feedId] - JWT payload feed identifier
95 * @param {string} [options.userId] - JWT payload user identifier
96 * @return {string} JWT Token
97 */
98121 var options = opts || {},
99 noTimestamp = options.expireTokens ? !options.expireTokens : true;
100121 var payload = {
101 resource: resource,
102 action: action,
103 };
104
105121 if (options.feedId) {
106119 payload['feed_id'] = options.feedId;
107 }
108
109121 if (options.userId) {
1102 payload['user_id'] = options.userId;
111 }
112
113121 var token = jwt.sign(payload, apiSecret, { algorithm: 'HS256', noTimestamp: noTimestamp });
114121 return token;
115};
116
1171exports.isJWTSignature = function(signature) {
118 /**
119 * check if token is a valid JWT token
120 * @method isJWTSignature
121 * @memberof signing
122 * @private
123 * @param {string} signature - Signature to check
124 * @return {boolean}
125 */
12668 var token = signature.split(' ')[1] || signature;
12768 return JWS_REGEX.test(token) && !!headerFromJWS(token);
128};
129

/Users/thierryschellenbach/workspace/stream-js/src/lib/utils.js

96%
28
27
1
LineHitsSource
11var errors = require('./errors');
21var validRe = /^[\w-]+$/;
3
41function validateFeedId(feedId) {
5 /*
6 * Validate that the feedId matches the spec user:1
7 */
82 var parts = feedId.split(':');
92 if (parts.length !== 2) {
101 throw new errors.FeedError('Invalid feedId, expected something like user:1 got ' + feedId);
11 }
12
131 var feedSlug = parts[0];
141 var userId = parts[1];
151 validateFeedSlug(feedSlug);
161 validateUserId(userId);
171 return feedId;
18}
19
201exports.validateFeedId = validateFeedId;
21
221function validateFeedSlug(feedSlug) {
23 /*
24 * Validate that the feedSlug matches \w
25 */
26558 var valid = validRe.test(feedSlug);
27558 if (!valid) {
282 throw new errors.FeedError('Invalid feedSlug, please use letters, numbers or _ got: ' + feedSlug);
29 }
30
31556 return feedSlug;
32}
33
341exports.validateFeedSlug = validateFeedSlug;
35
361function validateUserId(userId) {
37 /*
38 * Validate the userId matches \w
39 */
40556 var valid = validRe.test(userId);
41556 if (!valid) {
422 throw new errors.FeedError('Invalid feedSlug, please use letters, numbers or _ got: ' + userId);
43 }
44
45554 return userId;
46}
47
481exports.validateUserId = validateUserId;
49
501function rfc3986(str) {
512 return str.replace(/[!'()*]/g, function(c) {
520 return '%' + c.charCodeAt(0).toString(16).toUpperCase();
53 });
54}
55
561exports.rfc3986 = rfc3986;
57