1 var Util = require('util');
  2 var Client = require('./Client');
  3 var Constants = require('./Constants');
  4 
  5 /**
  6  * Creates an instance of StreamClient.
  7  *
  8  * @constructor
  9  * @this {StreamClient}
 10  * @param {String} consumerKey OAuth consumer key.
 11  * @param {String} consumerSecret OAuth consumer secret.
 12  * @param {String} token OAuth token.
 13  * @param {String} tokenSecret OAuth token secret.
 14  */
 15 var StreamClient = function(consumerKey, consumerSecret, token, tokenSecret)
 16 {
 17     Client.call(this, consumerKey, consumerSecret, token, tokenSecret);
 18 
 19     this._apiBaseUrlString = Constants.StreamApiBaseURLString;
 20     this._apiVersion = Constants.StreamApiVersion;
 21 };
 22 
 23 Util.inherits(StreamClient, Client);
 24 
 25 /**
 26  * Returns whether or not the stream client is currently running.
 27  *
 28  * @this {StreamClient}
 29  * @return true if the StreamClient is running; false otherwise.
 30  */
 31 StreamClient.prototype.isRunning = function()
 32 {
 33     for (var connection in this._connections)
 34     {
 35         return true;
 36     }
 37 
 38     return false;
 39 }
 40 
 41 /**
 42  * Creates an asynchronous connection to the Twitter Stream API and begins 
 43  * listening for public statuses that match one or more filter predicates.
 44  *
 45  * Listeners should be attached to the <code>StreamClient</code> instance in
 46  * order to respond to events:
 47  *
 48  * <pre>
 49  * // Request to remove geolocation information from a status
 50  * twitterStreamClient.on('deleteLocation', function(data) {
 51  *   console.log(data);
 52  * });
 53  *
 54  * // Status deletion request
 55  * twitterStreamClient.on('deleteTweet', function(data) {
 56  *   console.log(data);
 57  * });
 58  *
 59  * // Connection to the stream has been closed
 60  * twitterStreamClient.on('end', function() {
 61  *   console.log('Connection closed.');
 62  * });
 63  *
 64  * // An error has occurred
 65  * twitterStreamClient.on('error', function(error) {
 66  *   console.log('Error: ' + error.code ? error.code + ' ' + error.message : error.message);
 67  * });
 68  *
 69  * // A retweet has been received
 70  * twitterStreamClient.on('retweet', function(retweet) {
 71  *   console.log(retweet);
 72  * });
 73  *
 74  * // A tweet has been received
 75  * twitterStreamClient.on('tweet', function(tweet) {
 76  *   console.log(tweet);
 77  * });
 78  * </pre>
 79  *
 80  * See <a href="https://dev.twitter.com/docs/streaming-api/concepts">Twitter Streaming API Concepts</a>
 81  * for information on the structure of the JSON responses returned from the Twitter Streaming API.
 82  *
 83  * @this {StreamClient}
 84  * @param {Array} keywords A set of keywords to track.
 85  * @param {Array} locations A set of one or more latitude/longitude pairs defining geofences to track.
 86  * @param {Array} users A set of users to track.
 87  * @param {integer} count Number of previous statuses to deliver before transitioning to live stream delivery.
 88  */
 89 StreamClient.prototype.start = function(keywords, locations, users, count)
 90 {
 91     if (this.isRunning() === true)
 92     {
 93         throw new Error('StreamClient is currently running.');
 94     }
 95 
 96     var parameters = {};
 97 
 98     if (keywords !== undefined && keywords !== null)
 99     {
100         if (keywords instanceof Array)
101         {
102             parameters['track'] = keywords.join(',');
103         }
104         else
105         {
106             throw new Error('Expected Array object.');
107         }
108     }
109 
110     if (locations !== undefined && locations !== null)
111     {
112         if (locations instanceof Array)
113         {
114             parameters['locations'] = locations.join(',');
115         }
116         else
117         {
118             throw new Error('Expected Array object.');
119         }
120     }
121 
122     if (users !== undefined && users !== null)
123     {
124         if (users instanceof Array)
125         {
126             parameters['follow'] = users.join(',');
127         }
128         else
129         {
130             throw new Error('Expected Array object.');
131         }
132     }
133 
134     if (count !== undefined && count !== null)
135     {
136         if (isNaN(count) === false)
137         {
138             parameters['count'] = count;
139         }
140         else
141         {
142             throw new Error('Expected integer.');
143         }
144     }
145 
146     this._createPostRequest('statuses/filter', 'json', parameters);
147 };
148 
149 /**
150  * Disconnects from the Twitter Streaming API.
151  *
152  * @this {StreamClient}
153  */
154 StreamClient.prototype.stop = function()
155 {
156     for (var connection in this._connections)
157     {
158         connection.end();
159     }
160 };
161 
162 /**
163  * Handles the ending of a request.
164  *
165  * @private
166  * @this {StreamClient}
167  * @param {http.ClientRequest} aRequest The request object.
168  */
169 StreamClient.prototype._requestDidClose = function(aRequest)
170 {
171     Client.prototype._requestDidClose.call(this, aRequest);
172 
173     this.emit('close');
174 };
175 
176 /**
177  * Handles the closing of a request.
178  *
179  * @private
180  * @this {StreamClient}
181  * @param {http.ClientRequest} aRequest The request object.
182  */
183 StreamClient.prototype._requestDidEnd = function(aRequest)
184 {
185     Client.prototype._requestDidEnd.call(this, aRequest);
186 
187     this.emit('end');
188 };
189 
190 /**
191  * Handles the failure of a request.
192  *
193  * @private
194  * @this {StreamClient}
195  * @param {http.ClientRequest} aRequest The request object.
196  * @param {Error} aError An Error object.
197  */
198 StreamClient.prototype._requestDidFailWithError = function(aRequest, aError)
199 {
200     Client.prototype._requestDidFailWithError.call(this, aRequest, aError);
201 
202     this.emit('error', aError);
203 };
204 
205 /**
206  * Handles data received from the Twitter stream.
207  *
208  * @private
209  * @this {StreamClient}
210  * @param {http.ClientRequest} aRequest The request object.
211  * @param {Buffer} aData
212  */
213 StreamClient.prototype._requestDidReceiveData = function(aRequest, aData)
214 {
215     var connection = this._connectionForKey(aRequest.hash);
216     if (connection === undefined) return;
217 
218     connection['data'] = connection['data'] + aData.toString('utf8');
219 
220     var index = -1;
221     while ((index = connection['data'].indexOf(Constants.StreamApiObjectTerminator)) !== -1)
222     {
223         var jsonString = connection['data'].slice(0, index);
224         connection['data'] = connection['data'].slice(index + Constants.StreamApiObjectTerminator.length);
225         var object = JSON.parse(jsonString);
226 
227         if (object.delete !== undefined)
228         {
229             this.emit('deleteTweet', object.delete);
230         }
231         else if (object.scrub_geo !== undefined)
232         {
233             this.emit('deleteLocation', object.scrub_geo);
234         }
235         else if (object.limit !== undefined)
236         {
237             this.emit('limit', object.limit);
238         }
239         else if (object.retweeted_status !== undefined)
240         {
241             this.emit('retweet', object.retweeted_status);
242         }
243         else
244         {
245             this.emit('tweet', object);
246         }
247     }
248 };
249 
250 module.exports = StreamClient;
251