mirror of
https://github.com/thewesker/bug-em.git
synced 2025-12-22 13:01:05 -05:00
lol
This commit is contained in:
3
node_modules/twit/.npmignore
generated
vendored
Normal file
3
node_modules/twit/.npmignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
config*.js
|
||||
test.js
|
||||
647
node_modules/twit/README.md
generated
vendored
Normal file
647
node_modules/twit/README.md
generated
vendored
Normal file
@@ -0,0 +1,647 @@
|
||||
#twit
|
||||
|
||||
Twitter API Client for node
|
||||
|
||||
Supports both the **REST** and **Streaming** API.
|
||||
|
||||
#Installing
|
||||
|
||||
```
|
||||
npm install twit
|
||||
```
|
||||
|
||||
##Usage:
|
||||
|
||||
```javascript
|
||||
var Twit = require('twit')
|
||||
|
||||
var T = new Twit({
|
||||
consumer_key: '...',
|
||||
consumer_secret: '...',
|
||||
access_token: '...',
|
||||
access_token_secret: '...',
|
||||
timeout_ms: 60*1000, // optional HTTP request timeout to apply to all requests.
|
||||
})
|
||||
|
||||
//
|
||||
// tweet 'hello world!'
|
||||
//
|
||||
T.post('statuses/update', { status: 'hello world!' }, function(err, data, response) {
|
||||
console.log(data)
|
||||
})
|
||||
|
||||
//
|
||||
// search twitter for all tweets containing the word 'banana' since July 11, 2011
|
||||
//
|
||||
T.get('search/tweets', { q: 'banana since:2011-07-11', count: 100 }, function(err, data, response) {
|
||||
console.log(data)
|
||||
})
|
||||
|
||||
//
|
||||
// get the list of user id's that follow @tolga_tezel
|
||||
//
|
||||
T.get('followers/ids', { screen_name: 'tolga_tezel' }, function (err, data, response) {
|
||||
console.log(data)
|
||||
})
|
||||
|
||||
//
|
||||
// Twit has promise support; you can use the callback API,
|
||||
// promise API, or both at the same time.
|
||||
//
|
||||
T.get('account/verify_credentials', { skip_status: true })
|
||||
.catch(function (err) {
|
||||
console.log('caught error', err.stack)
|
||||
})
|
||||
.then(function (result) {
|
||||
// `result` is an Object with keys "data" and "resp".
|
||||
// `data` and `resp` are the same objects as the ones passed
|
||||
// to the callback.
|
||||
// See https://github.com/ttezel/twit#tgetpath-params-callback
|
||||
// for details.
|
||||
|
||||
console.log('data', result.data);
|
||||
})
|
||||
|
||||
//
|
||||
// retweet a tweet with id '343360866131001345'
|
||||
//
|
||||
T.post('statuses/retweet/:id', { id: '343360866131001345' }, function (err, data, response) {
|
||||
console.log(data)
|
||||
})
|
||||
|
||||
//
|
||||
// destroy a tweet with id '343360866131001345'
|
||||
//
|
||||
T.post('statuses/destroy/:id', { id: '343360866131001345' }, function (err, data, response) {
|
||||
console.log(data)
|
||||
})
|
||||
|
||||
//
|
||||
// get `funny` twitter users
|
||||
//
|
||||
T.get('users/suggestions/:slug', { slug: 'funny' }, function (err, data, response) {
|
||||
console.log(data)
|
||||
})
|
||||
|
||||
//
|
||||
// post a tweet with media
|
||||
//
|
||||
var b64content = fs.readFileSync('/path/to/img', { encoding: 'base64' })
|
||||
|
||||
// first we must post the media to Twitter
|
||||
T.post('media/upload', { media_data: b64content }, function (err, data, response) {
|
||||
// now we can assign alt text to the media, for use by screen readers and
|
||||
// other text-based presentations and interpreters
|
||||
var mediaIdStr = data.media_id_string
|
||||
var altText = "Small flowers in a planter on a sunny balcony, blossoming."
|
||||
var meta_params = { media_id: mediaIdStr, alt_text: { text: altText } }
|
||||
|
||||
T.post('media/metadata/create', meta_params, function (err, data, response) {
|
||||
if (!err) {
|
||||
// now we can reference the media and post a tweet (media will attach to the tweet)
|
||||
var params = { status: 'loving life #nofilter', media_ids: [mediaIdStr] }
|
||||
|
||||
T.post('statuses/update', params, function (err, data, response) {
|
||||
console.log(data)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
//
|
||||
// post media via the chunked media upload API.
|
||||
// You can then use POST statuses/update to post a tweet with the media attached as in the example above using `media_id_string`.
|
||||
// Note: You can also do this yourself manually using T.post() calls if you want more fine-grained
|
||||
// control over the streaming. Example: https://github.com/ttezel/twit/blob/master/tests/rest_chunked_upload.js#L20
|
||||
//
|
||||
var filePath = '/absolute/path/to/file.mp4'
|
||||
T.postMediaChunked({ file_path: filePath }, function (err, data, response) {
|
||||
console.log(data)
|
||||
})
|
||||
|
||||
//
|
||||
// stream a sample of public statuses
|
||||
//
|
||||
var stream = T.stream('statuses/sample')
|
||||
|
||||
stream.on('tweet', function (tweet) {
|
||||
console.log(tweet)
|
||||
})
|
||||
|
||||
//
|
||||
// filter the twitter public stream by the word 'mango'.
|
||||
//
|
||||
var stream = T.stream('statuses/filter', { track: 'mango' })
|
||||
|
||||
stream.on('tweet', function (tweet) {
|
||||
console.log(tweet)
|
||||
})
|
||||
|
||||
//
|
||||
// filter the public stream by the latitude/longitude bounded box of San Francisco
|
||||
//
|
||||
var sanFrancisco = [ '-122.75', '36.8', '-121.75', '37.8' ]
|
||||
|
||||
var stream = T.stream('statuses/filter', { locations: sanFrancisco })
|
||||
|
||||
stream.on('tweet', function (tweet) {
|
||||
console.log(tweet)
|
||||
})
|
||||
|
||||
//
|
||||
// filter the public stream by english tweets containing `#apple`
|
||||
//
|
||||
var stream = T.stream('statuses/filter', { track: '#apple', language: 'en' })
|
||||
|
||||
stream.on('tweet', function (tweet) {
|
||||
console.log(tweet)
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
# twit API:
|
||||
|
||||
##`var T = new Twit(config)`
|
||||
|
||||
Create a `Twit` instance that can be used to make requests to Twitter's APIs.
|
||||
|
||||
If authenticating with user context, `config` should be an object of the form:
|
||||
```
|
||||
{
|
||||
consumer_key: '...'
|
||||
, consumer_secret: '...'
|
||||
, access_token: '...'
|
||||
, access_token_secret: '...'
|
||||
}
|
||||
```
|
||||
|
||||
If authenticating with application context, `config` should be an object of the form:
|
||||
```
|
||||
{
|
||||
consumer_key: '...'
|
||||
, consumer_secret: '...'
|
||||
, app_only_auth: true
|
||||
}
|
||||
```
|
||||
Note that Application-only auth will not allow you to perform requests to API endpoints requiring
|
||||
a user context, such as posting tweets. However, the endpoints available can have a higher rate limit.
|
||||
|
||||
##`T.get(path, [params], callback)`
|
||||
GET any of the REST API endpoints.
|
||||
|
||||
**path**
|
||||
|
||||
The endpoint to hit. When specifying `path` values, omit the **'.json'** at the end (i.e. use **'search/tweets'** instead of **'search/tweets.json'**).
|
||||
|
||||
**params**
|
||||
|
||||
(Optional) parameters for the request.
|
||||
|
||||
**callback**
|
||||
|
||||
`function (err, data, response)`
|
||||
|
||||
- `data` is the parsed data received from Twitter.
|
||||
- `response` is the [http.IncomingMessage](http://nodejs.org/api/http.html#http_http_incomingmessage) received from Twitter.
|
||||
|
||||
##`T.post(path, [params], callback)`
|
||||
|
||||
POST any of the REST API endpoints. Same usage as `T.get()`.
|
||||
|
||||
##`T.postMediaChunked(params, callback)`
|
||||
|
||||
Helper function to post media via the POST media/upload (chunked) API. `params` is an object containing a `file_path` key. `file_path` is the absolute path to the file you want to upload.
|
||||
|
||||
```js
|
||||
var filePath = '/absolute/path/to/file.mp4'
|
||||
T.postMediaChunked({ file_path: filePath }, function (err, data, response) {
|
||||
console.log(data)
|
||||
})
|
||||
```
|
||||
|
||||
You can also use the POST media/upload API via T.post() calls if you want more fine-grained control over the streaming; [see here for an example](https://github.com/ttezel/twit/blob/master/tests/rest_chunked_upload.js#L20).
|
||||
|
||||
##`T.getAuth()`
|
||||
Get the client's authentication tokens.
|
||||
|
||||
##`T.setAuth(tokens)`
|
||||
Update the client's authentication tokens.
|
||||
|
||||
##`T.stream(path, [params])`
|
||||
Use this with the Streaming API.
|
||||
|
||||
**path**
|
||||
|
||||
Streaming endpoint to hit. One of:
|
||||
|
||||
- **'statuses/filter'**
|
||||
- **'statuses/sample'**
|
||||
- **'statuses/firehose'**
|
||||
- **'user'**
|
||||
- **'site'**
|
||||
|
||||
For a description of each Streaming endpoint, see the [Twitter API docs](https://dev.twitter.com/streaming/overview).
|
||||
|
||||
**params**
|
||||
|
||||
(Optional) parameters for the request. Any Arrays passed in `params` get converted to comma-separated strings, allowing you to do requests like:
|
||||
|
||||
```javascript
|
||||
//
|
||||
// I only want to see tweets about my favorite fruits
|
||||
//
|
||||
|
||||
// same result as doing { track: 'bananas,oranges,strawberries' }
|
||||
var stream = T.stream('statuses/filter', { track: ['bananas', 'oranges', 'strawberries'] })
|
||||
|
||||
stream.on('tweet', function (tweet) {
|
||||
//...
|
||||
})
|
||||
```
|
||||
|
||||
# Using the Streaming API
|
||||
|
||||
`T.stream(path, [params])` keeps the connection alive, and returns an `EventEmitter`.
|
||||
|
||||
The following events are emitted:
|
||||
|
||||
##event: 'message'
|
||||
|
||||
Emitted each time an object is received in the stream. This is a catch-all event that can be used to process any data received in the stream, rather than using the more specific events documented below.
|
||||
New in version 2.1.0.
|
||||
|
||||
```javascript
|
||||
stream.on('message', function (msg) {
|
||||
//...
|
||||
})
|
||||
```
|
||||
|
||||
##event: 'tweet'
|
||||
|
||||
Emitted each time a status (tweet) comes into the stream.
|
||||
|
||||
```javascript
|
||||
stream.on('tweet', function (tweet) {
|
||||
//...
|
||||
})
|
||||
```
|
||||
|
||||
##event: 'delete'
|
||||
|
||||
Emitted each time a status (tweet) deletion message comes into the stream.
|
||||
|
||||
```javascript
|
||||
stream.on('delete', function (deleteMessage) {
|
||||
//...
|
||||
})
|
||||
```
|
||||
|
||||
##event: 'limit'
|
||||
|
||||
Emitted each time a limitation message comes into the stream.
|
||||
|
||||
```javascript
|
||||
stream.on('limit', function (limitMessage) {
|
||||
//...
|
||||
})
|
||||
```
|
||||
|
||||
##event: 'scrub_geo'
|
||||
|
||||
Emitted each time a location deletion message comes into the stream.
|
||||
|
||||
```javascript
|
||||
stream.on('scrub_geo', function (scrubGeoMessage) {
|
||||
//...
|
||||
})
|
||||
```
|
||||
|
||||
##event: 'disconnect'
|
||||
|
||||
Emitted when a disconnect message comes from Twitter. This occurs if you have multiple streams connected to Twitter's API. Upon receiving a disconnect message from Twitter, `Twit` will close the connection and emit this event with the message details received from twitter.
|
||||
|
||||
```javascript
|
||||
stream.on('disconnect', function (disconnectMessage) {
|
||||
//...
|
||||
})
|
||||
```
|
||||
|
||||
##event: 'connect'
|
||||
|
||||
Emitted when a connection attempt is made to Twitter. The http `request` object is emitted.
|
||||
|
||||
```javascript
|
||||
stream.on('connect', function (request) {
|
||||
//...
|
||||
})
|
||||
```
|
||||
|
||||
##event: 'connected'
|
||||
|
||||
Emitted when the response is received from Twitter. The http `response` object is emitted.
|
||||
|
||||
```javascript
|
||||
stream.on('connected', function (response) {
|
||||
//...
|
||||
})
|
||||
```
|
||||
|
||||
##event: 'reconnect'
|
||||
|
||||
Emitted when a reconnection attempt to Twitter is scheduled. If Twitter is having problems or we get rate limited, we schedule a reconnect according to Twitter's [reconnection guidelines](https://dev.twitter.com/streaming/overview/connecting). The last http `request` and `response` objects are emitted, along with the time (in milliseconds) left before the reconnect occurs.
|
||||
|
||||
```javascript
|
||||
stream.on('reconnect', function (request, response, connectInterval) {
|
||||
//...
|
||||
})
|
||||
```
|
||||
|
||||
##event: 'warning'
|
||||
|
||||
This message is appropriate for clients using high-bandwidth connections, like the firehose. If your connection is falling behind, Twitter will queue messages for you, until your queue fills up, at which point they will disconnect you.
|
||||
|
||||
```javascript
|
||||
stream.on('warning', function (warning) {
|
||||
//...
|
||||
})
|
||||
```
|
||||
|
||||
##event: 'status_withheld'
|
||||
|
||||
Emitted when Twitter sends back a `status_withheld` message in the stream. This means that a tweet was withheld in certain countries.
|
||||
|
||||
```javascript
|
||||
stream.on('status_withheld', function (withheldMsg) {
|
||||
//...
|
||||
})
|
||||
```
|
||||
|
||||
##event: 'user_withheld'
|
||||
|
||||
Emitted when Twitter sends back a `user_withheld` message in the stream. This means that a Twitter user was withheld in certain countries.
|
||||
|
||||
```javascript
|
||||
stream.on('user_withheld', function (withheldMsg) {
|
||||
//...
|
||||
})
|
||||
```
|
||||
|
||||
##event: 'friends'
|
||||
|
||||
Emitted when Twitter sends the ["friends" preamble](https://dev.twitter.com/streaming/overview/messages-types#user_stream_messsages) when connecting to a user stream. This message contains a list of the user's friends, represented as an array of user ids. If the [stringify_friend_ids](https://dev.twitter.com/streaming/overview/request-parameters#stringify_friend_id) parameter is set, the friends
|
||||
list preamble will be returned as Strings (instead of Numbers).
|
||||
|
||||
```javascript
|
||||
var stream = T.stream('user', { stringify_friend_ids: true })
|
||||
stream.on('friends', function (friendsMsg) {
|
||||
//...
|
||||
})
|
||||
```
|
||||
|
||||
##event: 'direct_message'
|
||||
|
||||
Emitted when a direct message is sent to the user. Unfortunately, Twitter has not documented this event for user streams.
|
||||
|
||||
```javascript
|
||||
stream.on('direct_message', function (directMsg) {
|
||||
//...
|
||||
})
|
||||
```
|
||||
|
||||
##event: 'user_event'
|
||||
|
||||
Emitted when Twitter sends back a [User stream event](https://dev.twitter.com/streaming/overview/messages-types#Events_event).
|
||||
See the Twitter docs for more information on each event's structure.
|
||||
|
||||
```javascript
|
||||
stream.on('user_event', function (eventMsg) {
|
||||
//...
|
||||
})
|
||||
```
|
||||
|
||||
In addition, the following user stream events are provided for you to listen on:
|
||||
|
||||
* `blocked`
|
||||
* `unblocked`
|
||||
* `favorite`
|
||||
* `unfavorite`
|
||||
* `follow`
|
||||
* `unfollow`
|
||||
* `user_update`
|
||||
* `list_created`
|
||||
* `list_destroyed`
|
||||
* `list_updated`
|
||||
* `list_member_added`
|
||||
* `list_member_removed`
|
||||
* `list_user_subscribed`
|
||||
* `list_user_unsubscribed`
|
||||
* `quoted_tweet`
|
||||
* `retweeted_retweet`
|
||||
* `favorited_retweet`
|
||||
* `unknown_user_event` (for an event that doesn't match any of the above)
|
||||
|
||||
###Example:
|
||||
|
||||
```javascript
|
||||
stream.on('favorite', function (event) {
|
||||
//...
|
||||
})
|
||||
```
|
||||
|
||||
##event: 'error'
|
||||
|
||||
Emitted when an API request or response error occurs.
|
||||
An `Error` object is emitted, with properties:
|
||||
|
||||
```js
|
||||
{
|
||||
message: '...', // error message
|
||||
statusCode: '...', // statusCode from Twitter
|
||||
code: '...', // error code from Twitter
|
||||
twitterReply: '...', // raw response data from Twitter
|
||||
allErrors: '...' // array of errors returned from Twitter
|
||||
}
|
||||
```
|
||||
|
||||
##stream.stop()
|
||||
|
||||
Call this function on the stream to stop streaming (closes the connection with Twitter).
|
||||
|
||||
##stream.start()
|
||||
|
||||
Call this function to restart the stream after you called `.stop()` on it.
|
||||
Note: there is no need to call `.start()` to begin streaming. `Twit.stream` calls `.start()` for you.
|
||||
|
||||
-------
|
||||
|
||||
#What do I have access to?
|
||||
|
||||
Anything in the Twitter API:
|
||||
|
||||
* REST API Endpoints: https://dev.twitter.com/rest/public
|
||||
* Public stream endpoints: https://dev.twitter.com/streaming/public
|
||||
* User stream endpoints: https://dev.twitter.com/streaming/userstreams
|
||||
* Site stream endpoints: https://dev.twitter.com/streaming/sitestreams
|
||||
|
||||
-------
|
||||
|
||||
Go here to create an app and get OAuth credentials (if you haven't already): https://dev.twitter.com/apps/new
|
||||
|
||||
#Advanced
|
||||
|
||||
You may specify an array of trusted certificate fingerprints if you want to only trust a specific set of certificates.
|
||||
When an HTTP response is received, it is verified that the certificate was signed, and the peer certificate's fingerprint must be one of the values you specified. By default, the node.js trusted "root" CAs will be used.
|
||||
|
||||
eg.
|
||||
```js
|
||||
var twit = new Twit({
|
||||
consumer_key: '...',
|
||||
consumer_secret: '...',
|
||||
access_token: '...',
|
||||
access_token_secret: '...',
|
||||
trusted_cert_fingerprints: [
|
||||
'66:EA:47:62:D9:B1:4F:1A:AE:89:5F:68:BA:6B:8E:BB:F8:1D:BF:8E',
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
#Contributing
|
||||
|
||||
- Make your changes
|
||||
- Make sure your code matches the style of the code around it
|
||||
- Add tests that cover your feature/bugfix
|
||||
- Run tests
|
||||
- Submit a pull request
|
||||
|
||||
#How do I run the tests?
|
||||
|
||||
Create two files: `config1.js` and `config2.js` at the root of the `twit` folder. They should contain two different sets of oauth credentials for twit to use (two accounts are needed for testing interactions). They should both look something like this:
|
||||
|
||||
```
|
||||
module.exports = {
|
||||
consumer_key: '...'
|
||||
, consumer_secret: '...'
|
||||
, access_token: '...'
|
||||
, access_token_secret: '...'
|
||||
}
|
||||
```
|
||||
|
||||
Then run the tests:
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
You can also run the example:
|
||||
|
||||
```
|
||||
node examples/rtd2.js
|
||||
```
|
||||
|
||||

|
||||
|
||||
The example is a twitter bot named [RTD2](https://twitter.com/#!/iRTD2) written using `twit`. RTD2 tweets about **github** and curates its social graph.
|
||||
|
||||
-------
|
||||
|
||||
[FAQ](https://github.com/ttezel/twit/wiki/FAQ)
|
||||
|
||||
-------
|
||||
|
||||
## License
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) by Tolga Tezel <tolgatezel11@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
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.
|
||||
|
||||
## Changelog
|
||||
|
||||
###2.2.2
|
||||
* Emit `parser-error` instead of `error` event if Twitter sends back
|
||||
an uncompressed HTTP response body.
|
||||
|
||||
###2.2.1
|
||||
* Add promise support to Twit REST API calls.
|
||||
|
||||
###2.2.0
|
||||
* Allow omission of `new` keyword; `var t = Twit(config)` works, and `var t = new Twit(config)` works too.
|
||||
* Allow setting an array of trusted certificate fingerprints via `config.trusted_cert_fingerprints`.
|
||||
* Automatically adjust timestamp for OAuth'ed HTTP requests
|
||||
by recording the timestamp from Twitter HTTP responses, computing our local time offset, and applying the offset in the next HTTP request to Twitter.
|
||||
|
||||
###2.1.7
|
||||
* Add `mime` as a dependency.
|
||||
|
||||
###2.1.6
|
||||
* Emit `friends` event for `friends_str` message received when a user stream is requested with `stringify_friend_ids=true`.
|
||||
* Handle receiving "Exceeded connection limit for user" message from Twitter while streaming. Emit `error` event for this case.
|
||||
* Emit `retweeted_retweet` and `favorited_retweet` user events.
|
||||
* Add MIT license to package.json (about time!)
|
||||
|
||||
###2.1.5
|
||||
* Support config-based request timeout.
|
||||
|
||||
###2.1.4
|
||||
* Support POST media/upload (chunked) and add `T.postMediaChunked()` to make it easy.
|
||||
|
||||
###2.1.3
|
||||
* Fix bug in constructing HTTP requests for `account/update_profile_image` and `account/update_profile_background_image` paths.
|
||||
|
||||
###2.1.2
|
||||
* Enable gzip on network traffic
|
||||
* Add `quoted_tweet` event
|
||||
|
||||
###2.1.1
|
||||
* Strict-mode fixes (Twit can now be run with strict mode)
|
||||
* Fix handling of disconect message from Twitter
|
||||
* If Twitter returns a non-JSON-parseable fragment during streaming, emit 'parser-error' instead of 'error' (to discard fragments like "Internal Server Error")
|
||||
|
||||
###2.1.0
|
||||
* Add `message` event.
|
||||
|
||||
###2.0.0
|
||||
* Implement Application-only auth
|
||||
* Remove oauth module as a dependency
|
||||
|
||||
###1.1.20
|
||||
* Implement support for POST /media/upload
|
||||
* Reconnect logic fix for streaming; add stall abort/reconnect timeout on first connection attempt.
|
||||
|
||||
###1.1.14
|
||||
* Emit `connected` event upon receiving the response from twitter
|
||||
|
||||
###1.0.0
|
||||
* now to stop and start the stream, use `stream.stop()` and `stream.start()` instead of emitting the `start` and `stop` events
|
||||
* If twitter sends a `disconnect` message, closes the stream and emits `disconnect` with the disconnect message received from twitter
|
||||
|
||||
###0.2.0
|
||||
* Updated `twit` for usage with v1.1 of the Twitter API.
|
||||
|
||||
###0.1.5
|
||||
|
||||
* **BREAKING CHANGE** to `twit.stream()`. Does not take a callback anymore. It returns
|
||||
immediately with the `EventEmitter` that you can listen on. The `Usage` section in
|
||||
the Readme.md has been updated. Read it.
|
||||
|
||||
|
||||
###0.1.4
|
||||
|
||||
* `twit.stream()` has signature `function (path, params, callback)`
|
||||
127
node_modules/twit/examples/bot.js
generated
vendored
Normal file
127
node_modules/twit/examples/bot.js
generated
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
//
|
||||
// Bot
|
||||
// class for performing various twitter actions
|
||||
//
|
||||
var Twit = require('../lib/twitter');
|
||||
|
||||
var Bot = module.exports = function(config) {
|
||||
this.twit = new Twit(config);
|
||||
};
|
||||
|
||||
//
|
||||
// post a tweet
|
||||
//
|
||||
Bot.prototype.tweet = function (status, callback) {
|
||||
if(typeof status !== 'string') {
|
||||
return callback(new Error('tweet must be of type String'));
|
||||
} else if(status.length > 140) {
|
||||
return callback(new Error('tweet is too long: ' + status.length));
|
||||
}
|
||||
this.twit.post('statuses/update', { status: status }, callback);
|
||||
};
|
||||
|
||||
Bot.prototype.searchFollow = function (params, callback) {
|
||||
var self = this;
|
||||
|
||||
self.twit.get('search/tweets', params, function (err, reply) {
|
||||
if(err) return callback(err);
|
||||
|
||||
var tweets = reply.statuses;
|
||||
var rTweet = randIndex(tweets)
|
||||
if(typeof rTweet != 'undefined')
|
||||
{
|
||||
var target = rTweet.user.id_str;
|
||||
|
||||
self.twit.post('friendships/create', { id: target }, callback);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// retweet
|
||||
//
|
||||
Bot.prototype.retweet = function (params, callback) {
|
||||
var self = this;
|
||||
|
||||
self.twit.get('search/tweets', params, function (err, reply) {
|
||||
if(err) return callback(err);
|
||||
|
||||
var tweets = reply.statuses;
|
||||
var randomTweet = randIndex(tweets);
|
||||
if(typeof randomTweet != 'undefined')
|
||||
self.twit.post('statuses/retweet/:id', { id: randomTweet.id_str }, callback);
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// favorite a tweet
|
||||
//
|
||||
Bot.prototype.favorite = function (params, callback) {
|
||||
var self = this;
|
||||
|
||||
self.twit.get('search/tweets', params, function (err, reply) {
|
||||
if(err) return callback(err);
|
||||
|
||||
var tweets = reply.statuses;
|
||||
var randomTweet = randIndex(tweets);
|
||||
if(typeof randomTweet != 'undefined')
|
||||
self.twit.post('favorites/create', { id: randomTweet.id_str }, callback);
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// choose a random friend of one of your followers, and follow that user
|
||||
//
|
||||
Bot.prototype.mingle = function (callback) {
|
||||
var self = this;
|
||||
|
||||
this.twit.get('followers/ids', function(err, reply) {
|
||||
if(err) { return callback(err); }
|
||||
|
||||
var followers = reply.ids
|
||||
, randFollower = randIndex(followers);
|
||||
|
||||
self.twit.get('friends/ids', { user_id: randFollower }, function(err, reply) {
|
||||
if(err) { return callback(err); }
|
||||
|
||||
var friends = reply.ids
|
||||
, target = randIndex(friends);
|
||||
|
||||
self.twit.post('friendships/create', { id: target }, callback);
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
//
|
||||
// prune your followers list; unfollow a friend that hasn't followed you back
|
||||
//
|
||||
Bot.prototype.prune = function (callback) {
|
||||
var self = this;
|
||||
|
||||
this.twit.get('followers/ids', function(err, reply) {
|
||||
if(err) return callback(err);
|
||||
|
||||
var followers = reply.ids;
|
||||
|
||||
self.twit.get('friends/ids', function(err, reply) {
|
||||
if(err) return callback(err);
|
||||
|
||||
var friends = reply.ids
|
||||
, pruned = false;
|
||||
|
||||
while(!pruned) {
|
||||
var target = randIndex(friends);
|
||||
|
||||
if(!~followers.indexOf(target)) {
|
||||
pruned = true;
|
||||
self.twit.post('friendships/destroy', { id: target }, callback);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function randIndex (arr) {
|
||||
var index = Math.floor(arr.length*Math.random());
|
||||
return arr[index];
|
||||
};
|
||||
77
node_modules/twit/examples/rtd2.js
generated
vendored
Normal file
77
node_modules/twit/examples/rtd2.js
generated
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// RTD2 - Twitter bot that tweets about the most popular github.com news
|
||||
// Also makes new friends and prunes its followings.
|
||||
//
|
||||
var Bot = require('./bot')
|
||||
, config1 = require('../config1');
|
||||
|
||||
var bot = new Bot(config1);
|
||||
|
||||
console.log('RTD2: Running.');
|
||||
|
||||
//get date string for today's date (e.g. '2011-01-01')
|
||||
function datestring () {
|
||||
var d = new Date(Date.now() - 5*60*60*1000); //est timezone
|
||||
return d.getUTCFullYear() + '-'
|
||||
+ (d.getUTCMonth() + 1) + '-'
|
||||
+ d.getDate();
|
||||
};
|
||||
|
||||
setInterval(function() {
|
||||
bot.twit.get('followers/ids', function(err, reply) {
|
||||
if(err) return handleError(err)
|
||||
console.log('\n# followers:' + reply.ids.length.toString());
|
||||
});
|
||||
var rand = Math.random();
|
||||
|
||||
if(rand <= 0.10) { // tweet popular github tweet
|
||||
var params = {
|
||||
q: 'github.com/'
|
||||
, since: datestring()
|
||||
, result_type: 'mixed'
|
||||
};
|
||||
bot.twit.get('search/tweets', params, function (err, reply) {
|
||||
if(err) return handleError(err);
|
||||
|
||||
var max = 0, popular;
|
||||
|
||||
var tweets = reply.statuses
|
||||
, i = tweets.length;
|
||||
|
||||
while(i--) {
|
||||
var tweet = tweets[i]
|
||||
, popularity = tweet.retweet_count;
|
||||
|
||||
if(popularity > max) {
|
||||
max = popularity;
|
||||
popular = tweet.text;
|
||||
}
|
||||
}
|
||||
|
||||
bot.tweet(popular, function (err, reply) {
|
||||
if(err) return handleError(err);
|
||||
|
||||
console.log('\nTweet: ' + (reply ? reply.text : reply));
|
||||
})
|
||||
});
|
||||
} else if(rand <= 0.55) { // make a friend
|
||||
bot.mingle(function(err, reply) {
|
||||
if(err) return handleError(err);
|
||||
|
||||
var name = reply.screen_name;
|
||||
console.log('\nMingle: followed @' + name);
|
||||
});
|
||||
} else { // prune a friend
|
||||
bot.prune(function(err, reply) {
|
||||
if(err) return handleError(err);
|
||||
|
||||
var name = reply.screen_name
|
||||
console.log('\nPrune: unfollowed @'+ name);
|
||||
});
|
||||
}
|
||||
}, 40000);
|
||||
|
||||
function handleError(err) {
|
||||
console.error('response status:', err.statusCode);
|
||||
console.error('data:', err.data);
|
||||
}
|
||||
11
node_modules/twit/lib/endpoints.js
generated
vendored
Normal file
11
node_modules/twit/lib/endpoints.js
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// Twitter Endpoints
|
||||
module.exports = {
|
||||
API_HOST : 'https://api.twitter.com/'
|
||||
, REST_ROOT : 'https://api.twitter.com/1.1/'
|
||||
, PUB_STREAM : 'https://stream.twitter.com/1.1/'
|
||||
, USER_STREAM : 'https://userstream.twitter.com/1.1/'
|
||||
, SITE_STREAM : 'https://sitestream.twitter.com/1.1/'
|
||||
, MEDIA_UPLOAD : 'https://upload.twitter.com/1.1/'
|
||||
, OA_REQ : 'https://api.twitter.com/oauth/request_token'
|
||||
, OA_ACCESS : 'https://api.twitter.com/oauth/access_token'
|
||||
}
|
||||
143
node_modules/twit/lib/file_uploader.js
generated
vendored
Normal file
143
node_modules/twit/lib/file_uploader.js
generated
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
var assert = require('assert');
|
||||
var fs = require('fs');
|
||||
var mime = require('mime');
|
||||
var util = require('util');
|
||||
|
||||
var MAX_FILE_SIZE_BYTES = 15 * 1024 * 1024;
|
||||
var MAX_FILE_CHUNK_BYTES = 5 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* FileUploader class used to upload a file to twitter via the /media/upload (chunked) API.
|
||||
* Usage:
|
||||
* var fu = new FileUploader({ file_path: '/foo/bar/baz.mp4' }, twit);
|
||||
* fu.upload(function (err, bodyObj, resp) {
|
||||
* console.log(err, bodyObj);
|
||||
* })
|
||||
*
|
||||
* @param {Object} params Object of the form { file_path: String }.
|
||||
* @param {Twit(object)} twit Twit instance.
|
||||
*/
|
||||
var FileUploader = function (params, twit) {
|
||||
assert(params)
|
||||
assert(params.file_path, 'Must specify `file_path` to upload a file. Got: ' + params.file_path + '.')
|
||||
var self = this;
|
||||
self._file_path = params.file_path;
|
||||
self._twit = twit;
|
||||
self._isUploading = false;
|
||||
self._isFileStreamEnded = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to Twitter via the /media/upload (chunked) API.
|
||||
*
|
||||
* @param {Function} cb function (err, data, resp)
|
||||
*/
|
||||
FileUploader.prototype.upload = function (cb) {
|
||||
var self = this;
|
||||
|
||||
// Send INIT command with file info and get back a media_id_string we can use to APPEND chunks to it.
|
||||
self._initMedia(function (err, bodyObj, resp) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
} else {
|
||||
var mediaTmpId = bodyObj.media_id_string;
|
||||
var chunkNumber = 0;
|
||||
var mediaFile = fs.createReadStream(self._file_path, { highWatermark: MAX_FILE_CHUNK_BYTES });
|
||||
|
||||
mediaFile.on('data', function (chunk) {
|
||||
// Pause our file stream from emitting `data` events until the upload of this chunk completes.
|
||||
// Any data that becomes available will remain in the internal buffer.
|
||||
mediaFile.pause();
|
||||
self._isUploading = true;
|
||||
|
||||
self._appendMedia(mediaTmpId, chunk.toString('base64'), chunkNumber, function (err, bodyObj, resp) {
|
||||
self._isUploading = false;
|
||||
if (err) {
|
||||
cb(err);
|
||||
} else {
|
||||
if (self._isUploadComplete()) {
|
||||
// We've hit the end of our stream; send FINALIZE command.
|
||||
self._finalizeMedia(mediaTmpId, cb);
|
||||
} else {
|
||||
// Tell our file stream to start emitting `data` events again.
|
||||
chunkNumber++;
|
||||
mediaFile.resume();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
mediaFile.on('end', function () {
|
||||
// Mark our file streaming complete, and if done, send FINALIZE command.
|
||||
self._isFileStreamEnded = true;
|
||||
if (self._isUploadComplete()) {
|
||||
self._finalizeMedia(mediaTmpId, cb);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
FileUploader.prototype._isUploadComplete = function () {
|
||||
return !this._isUploading && this._isFileStreamEnded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send FINALIZE command for media object with id `media_id`.
|
||||
*
|
||||
* @param {String} media_id
|
||||
* @param {Function} cb
|
||||
*/
|
||||
FileUploader.prototype._finalizeMedia = function(media_id, cb) {
|
||||
var self = this;
|
||||
self._twit.post('media/upload', {
|
||||
command: 'FINALIZE',
|
||||
media_id: media_id
|
||||
}, cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send APPEND command for media object with id `media_id`.
|
||||
* Append the chunk to the media object, then resume streaming our mediaFile.
|
||||
*
|
||||
* @param {String} media_id media_id_string received from Twitter after sending INIT comand.
|
||||
* @param {String} chunk_part Base64-encoded String chunk of the media file.
|
||||
* @param {Number} segment_index Index of the segment.
|
||||
* @param {Function} cb
|
||||
*/
|
||||
FileUploader.prototype._appendMedia = function(media_id_string, chunk_part, segment_index, cb) {
|
||||
var self = this;
|
||||
self._twit.post('media/upload', {
|
||||
command: 'APPEND',
|
||||
media_id: media_id_string.toString(),
|
||||
segment_index: segment_index,
|
||||
media: chunk_part,
|
||||
}, cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send INIT command for our underlying media object.
|
||||
*
|
||||
* @param {Function} cb
|
||||
*/
|
||||
FileUploader.prototype._initMedia = function (cb) {
|
||||
var self = this;
|
||||
var mediaType = mime.lookup(self._file_path);
|
||||
var mediaFileSizeBytes = fs.statSync(self._file_path).size;
|
||||
|
||||
// Check the file size - it should not go over 15MB for video.
|
||||
// See https://dev.twitter.com/rest/reference/post/media/upload-chunked
|
||||
if (mediaFileSizeBytes < MAX_FILE_SIZE_BYTES) {
|
||||
self._twit.post('media/upload', {
|
||||
'command': 'INIT',
|
||||
'media_type': mediaType,
|
||||
'total_bytes': mediaFileSizeBytes
|
||||
}, cb);
|
||||
} else {
|
||||
var errMsg = util.format('This file is too large. Max size is %dB. Got: %dB.', MAX_FILE_SIZE_BYTES, mediaFileSizeBytes);
|
||||
cb(new Error(errMsg));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FileUploader
|
||||
128
node_modules/twit/lib/helpers.js
generated
vendored
Normal file
128
node_modules/twit/lib/helpers.js
generated
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
var querystring = require('querystring');
|
||||
var request = require('request');
|
||||
|
||||
var endpoints = require('./endpoints');
|
||||
|
||||
/**
|
||||
* Encodes object as a querystring, to be used as the suffix of request URLs.
|
||||
* @param {Object} obj
|
||||
* @return {String}
|
||||
*/
|
||||
exports.makeQueryString = function (obj) {
|
||||
var qs = querystring.stringify(obj)
|
||||
qs = qs.replace(/\!/g, "%21")
|
||||
.replace(/\'/g, "%27")
|
||||
.replace(/\(/g, "%28")
|
||||
.replace(/\)/g, "%29")
|
||||
.replace(/\*/g, "%2A");
|
||||
return qs
|
||||
}
|
||||
|
||||
/**
|
||||
* For each `/:param` fragment in path, move the value in params
|
||||
* at that key to path. If the key is not found in params, throw.
|
||||
* Modifies both params and path values.
|
||||
*
|
||||
* @param {Objet} params Object used to build path.
|
||||
* @param {String} path String to transform.
|
||||
* @return {Undefined}
|
||||
*
|
||||
*/
|
||||
exports.moveParamsIntoPath = function (params, path) {
|
||||
var rgxParam = /\/:(\w+)/g
|
||||
var missingParamErr = null
|
||||
|
||||
path = path.replace(rgxParam, function (hit) {
|
||||
var paramName = hit.slice(2)
|
||||
var suppliedVal = params[paramName]
|
||||
if (!suppliedVal) {
|
||||
throw new Error('Twit: Params object is missing a required parameter for this request: `'+paramName+'`')
|
||||
}
|
||||
var retVal = '/' + suppliedVal
|
||||
delete params[paramName]
|
||||
return retVal
|
||||
})
|
||||
return path
|
||||
}
|
||||
|
||||
/**
|
||||
* When Twitter returns a response that looks like an error response,
|
||||
* use this function to attach the error info in the response body to `err`.
|
||||
*
|
||||
* @param {Error} err Error instance to which body info will be attached
|
||||
* @param {Object} body JSON object that is the deserialized HTTP response body received from Twitter
|
||||
* @return {Undefined}
|
||||
*/
|
||||
exports.attachBodyInfoToError = function (err, body) {
|
||||
err.twitterReply = body;
|
||||
if (!body) {
|
||||
return
|
||||
}
|
||||
if (body.error) {
|
||||
// the body itself is an error object
|
||||
err.message = body.error
|
||||
err.allErrors = err.allErrors.concat([body])
|
||||
} else if (body.errors && body.errors.length) {
|
||||
// body contains multiple error objects
|
||||
err.message = body.errors[0].message;
|
||||
err.code = body.errors[0].code;
|
||||
err.allErrors = err.allErrors.concat(body.errors)
|
||||
}
|
||||
}
|
||||
|
||||
exports.makeTwitError = function (message) {
|
||||
var err = new Error()
|
||||
if (message) {
|
||||
err.message = message
|
||||
}
|
||||
err.code = null
|
||||
err.allErrors = []
|
||||
err.twitterReply = null
|
||||
return err
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a bearer token for OAuth2
|
||||
* @param {String} consumer_key
|
||||
* @param {String} consumer_secret
|
||||
* @param {Function} cb
|
||||
*
|
||||
* Calls `cb` with Error, String
|
||||
*
|
||||
* Error (if it exists) is guaranteed to be Twit error-formatted.
|
||||
* String (if it exists) is the bearer token received from Twitter.
|
||||
*/
|
||||
exports.getBearerToken = function (consumer_key, consumer_secret, cb) {
|
||||
// use OAuth 2 for app-only auth (Twitter requires this)
|
||||
// get a bearer token using our app's credentials
|
||||
var b64Credentials = new Buffer(consumer_key + ':' + consumer_secret).toString('base64');
|
||||
request.post({
|
||||
url: endpoints.API_HOST + '/oauth2/token',
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + b64Credentials,
|
||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
|
||||
},
|
||||
body: 'grant_type=client_credentials',
|
||||
json: true,
|
||||
}, function (err, res, body) {
|
||||
if (err) {
|
||||
var error = exports.makeTwitError(err.toString());
|
||||
exports.attachBodyInfoToError(error, body);
|
||||
return cb(error, body, res);
|
||||
}
|
||||
|
||||
if ( !body ) {
|
||||
var error = exports.makeTwitError('Not valid reply from Twitter upon obtaining bearer token');
|
||||
exports.attachBodyInfoToError(error, body);
|
||||
return cb(error, body, res);
|
||||
}
|
||||
|
||||
if (body.token_type !== 'bearer') {
|
||||
var error = exports.makeTwitError('Unexpected reply from Twitter upon obtaining bearer token');
|
||||
exports.attachBodyInfoToError(error, body);
|
||||
return cb(error, body, res);
|
||||
}
|
||||
|
||||
return cb(err, body.access_token);
|
||||
})
|
||||
}
|
||||
56
node_modules/twit/lib/parser.js
generated
vendored
Normal file
56
node_modules/twit/lib/parser.js
generated
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// Parser - for Twitter Streaming API
|
||||
//
|
||||
var util = require('util')
|
||||
, EventEmitter = require('events').EventEmitter;
|
||||
|
||||
var Parser = module.exports = function () {
|
||||
this.message = ''
|
||||
|
||||
EventEmitter.call(this);
|
||||
};
|
||||
|
||||
util.inherits(Parser, EventEmitter);
|
||||
|
||||
Parser.prototype.parse = function (chunk) {
|
||||
this.message += chunk;
|
||||
chunk = this.message;
|
||||
|
||||
var size = chunk.length
|
||||
, start = 0
|
||||
, offset = 0
|
||||
, curr
|
||||
, next;
|
||||
|
||||
while (offset < size) {
|
||||
curr = chunk[offset];
|
||||
next = chunk[offset + 1];
|
||||
|
||||
if (curr === '\r' && next === '\n') {
|
||||
var piece = chunk.slice(start, offset);
|
||||
start = offset += 2;
|
||||
|
||||
if (!piece.length) { continue; } //empty object
|
||||
|
||||
if (piece === 'Exceeded connection limit for user') {
|
||||
this.emit('connection-limit-exceeded',
|
||||
new Error('Twitter says: ' + piece + '. Only instantiate one stream per set of credentials.'));
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
var msg = JSON.parse(piece)
|
||||
} catch (err) {
|
||||
this.emit('error', new Error('Error parsing twitter reply: `'+piece+'`, error message `'+err+'`'));
|
||||
} finally {
|
||||
if (msg)
|
||||
this.emit('element', msg)
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
offset++;
|
||||
}
|
||||
|
||||
this.message = chunk.slice(start, size);
|
||||
};
|
||||
2
node_modules/twit/lib/settings.js
generated
vendored
Normal file
2
node_modules/twit/lib/settings.js
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
// set of status codes where we don't attempt reconnecting to Twitter
|
||||
exports.STATUS_CODES_TO_ABORT_ON = [ 400, 401, 403, 404, 406, 410, 422 ];
|
||||
358
node_modules/twit/lib/streaming-api-connection.js
generated
vendored
Normal file
358
node_modules/twit/lib/streaming-api-connection.js
generated
vendored
Normal file
@@ -0,0 +1,358 @@
|
||||
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var util = require('util');
|
||||
|
||||
var helpers = require('./helpers')
|
||||
var Parser = require('./parser');
|
||||
var request = require('request');
|
||||
var zlib = require('zlib');
|
||||
|
||||
var STATUS_CODES_TO_ABORT_ON = require('./settings').STATUS_CODES_TO_ABORT_ON
|
||||
|
||||
var StreamingAPIConnection = function (reqOpts, twitOptions) {
|
||||
this.reqOpts = reqOpts
|
||||
this.twitOptions = twitOptions
|
||||
this._twitter_time_minus_local_time_ms = 0
|
||||
EventEmitter.call(this)
|
||||
}
|
||||
|
||||
util.inherits(StreamingAPIConnection, EventEmitter)
|
||||
|
||||
/**
|
||||
* Resets the connection.
|
||||
* - clears request, response, parser
|
||||
* - removes scheduled reconnect handle (if one was scheduled)
|
||||
* - stops the stall abort timeout handle (if one was scheduled)
|
||||
*/
|
||||
StreamingAPIConnection.prototype._resetConnection = function () {
|
||||
if (this.request) {
|
||||
// clear our reference to the `request` instance
|
||||
this.request.removeAllListeners();
|
||||
this.request.destroy();
|
||||
}
|
||||
|
||||
if (this.response) {
|
||||
// clear our reference to the http.IncomingMessage instance
|
||||
this.response.removeAllListeners();
|
||||
this.response.destroy();
|
||||
}
|
||||
|
||||
if (this.parser) {
|
||||
this.parser.removeAllListeners()
|
||||
}
|
||||
|
||||
// ensure a scheduled reconnect does not occur (if one was scheduled)
|
||||
// this can happen if we get a close event before .stop() is called
|
||||
clearTimeout(this._scheduledReconnect)
|
||||
delete this._scheduledReconnect
|
||||
|
||||
// clear our stall abort timeout
|
||||
this._stopStallAbortTimeout()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the parameters used in determining the next reconnect time
|
||||
*/
|
||||
StreamingAPIConnection.prototype._resetRetryParams = function () {
|
||||
// delay for next reconnection attempt
|
||||
this._connectInterval = 0
|
||||
// flag indicating whether we used a 0-delay reconnect
|
||||
this._usedFirstReconnect = false
|
||||
}
|
||||
|
||||
StreamingAPIConnection.prototype._startPersistentConnection = function () {
|
||||
var self = this;
|
||||
self._resetConnection();
|
||||
self._setupParser();
|
||||
self._resetStallAbortTimeout();
|
||||
self._setOauthTimestamp();
|
||||
self.request = request.post(this.reqOpts);
|
||||
self.emit('connect', self.request);
|
||||
self.request.on('response', function (response) {
|
||||
self._updateOauthTimestampOffsetFromResponse(response)
|
||||
// reset our reconnection attempt flag so next attempt goes through with 0 delay
|
||||
// if we get a transport-level error
|
||||
self._usedFirstReconnect = false;
|
||||
// start a stall abort timeout handle
|
||||
self._resetStallAbortTimeout();
|
||||
self.response = response
|
||||
if (STATUS_CODES_TO_ABORT_ON.indexOf(self.response.statusCode) !== -1) {
|
||||
// We got a status code telling us we should abort the connection.
|
||||
// Read the body from the response and return an error to the user.
|
||||
var body = '';
|
||||
var compressedBody = '';
|
||||
|
||||
self.response.on('data', function (chunk) {
|
||||
compressedBody += chunk.toString('utf8');
|
||||
})
|
||||
|
||||
var gunzip = zlib.createGunzip();
|
||||
self.response.pipe(gunzip);
|
||||
gunzip.on('data', function (chunk) {
|
||||
body += chunk.toString('utf8')
|
||||
})
|
||||
|
||||
gunzip.on('end', function () {
|
||||
try {
|
||||
body = JSON.parse(body)
|
||||
} catch (jsonDecodeError) {
|
||||
// Twitter may send an HTML body
|
||||
// if non-JSON text was returned, we'll just attach it to the error as-is
|
||||
}
|
||||
// surface the error to the user
|
||||
var error = helpers.makeTwitError('Bad Twitter streaming request: ' + self.response.statusCode)
|
||||
error.statusCode = response ? response.statusCode: null;
|
||||
helpers.attachBodyInfoToError(error, body)
|
||||
self.emit('error', error);
|
||||
// stop the stream explicitly so we don't reconnect
|
||||
self.stop()
|
||||
body = null;
|
||||
});
|
||||
gunzip.on('error', function (err) {
|
||||
// If Twitter sends us back an uncompressed HTTP response, gzip will error out.
|
||||
// Handle this by emitting an error with the uncompressed response body.
|
||||
var errMsg = 'Gzip error: ' + err.message;
|
||||
var twitErr = helpers.makeTwitError(errMsg);
|
||||
twitErr.statusCode = self.response.statusCode;
|
||||
helpers.attachBodyInfoToError(twitErr, compressedBody);
|
||||
self.emit('parser-error', twitErr);
|
||||
});
|
||||
} else if (self.response.statusCode === 420) {
|
||||
// close the connection forcibly so a reconnect is scheduled by `self.onClose()`
|
||||
self._scheduleReconnect();
|
||||
} else {
|
||||
// We got an OK status code - the response should be valid.
|
||||
// Read the body from the response and return to the user.
|
||||
var gunzip = zlib.createGunzip();
|
||||
self.response.pipe(gunzip);
|
||||
|
||||
//pass all response data to parser
|
||||
gunzip.on('data', function (chunk) {
|
||||
self._connectInterval = 0
|
||||
// stop stall timer, and start a new one
|
||||
self._resetStallAbortTimeout();
|
||||
self.parser.parse(chunk.toString('utf8'));
|
||||
});
|
||||
|
||||
gunzip.on('close', self._onClose.bind(self))
|
||||
gunzip.on('error', function (err) {
|
||||
self.emit('error', err);
|
||||
})
|
||||
self.response.on('error', function (err) {
|
||||
// expose response errors on twit instance
|
||||
self.emit('error', err);
|
||||
})
|
||||
|
||||
// connected without an error response from Twitter, emit `connected` event
|
||||
// this must be emitted after all its event handlers are bound
|
||||
// so the reference to `self.response` is not interfered-with by the user until it is emitted
|
||||
self.emit('connected', self.response);
|
||||
}
|
||||
});
|
||||
self.request.on('close', self._onClose.bind(self));
|
||||
self.request.on('error', function (err) { self._scheduleReconnect.bind(self) });
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle when the request or response closes.
|
||||
* Schedule a reconnect according to Twitter's reconnect guidelines
|
||||
*
|
||||
*/
|
||||
StreamingAPIConnection.prototype._onClose = function () {
|
||||
var self = this;
|
||||
self._stopStallAbortTimeout();
|
||||
if (self._scheduledReconnect) {
|
||||
// if we already have a reconnect scheduled, don't schedule another one.
|
||||
// this race condition can happen if the http.ClientRequest and http.IncomingMessage both emit `close`
|
||||
return
|
||||
}
|
||||
|
||||
self._scheduleReconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Kick off the http request, and persist the connection
|
||||
*
|
||||
*/
|
||||
StreamingAPIConnection.prototype.start = function () {
|
||||
this._resetRetryParams();
|
||||
this._startPersistentConnection();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort the http request, stop scheduled reconnect (if one was scheduled) and clear state
|
||||
*
|
||||
*/
|
||||
StreamingAPIConnection.prototype.stop = function () {
|
||||
// clear connection variables and timeout handles
|
||||
this._resetConnection();
|
||||
this._resetRetryParams();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop and restart the stall abort timer (called when new data is received)
|
||||
*
|
||||
* If we go 90s without receiving data from twitter, we abort the request & reconnect.
|
||||
*/
|
||||
StreamingAPIConnection.prototype._resetStallAbortTimeout = function () {
|
||||
var self = this;
|
||||
// stop the previous stall abort timer
|
||||
self._stopStallAbortTimeout();
|
||||
//start a new 90s timeout to trigger a close & reconnect if no data received
|
||||
self._stallAbortTimeout = setTimeout(function () {
|
||||
self._scheduleReconnect()
|
||||
}, 90000);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop stall timeout
|
||||
*
|
||||
*/
|
||||
StreamingAPIConnection.prototype._stopStallAbortTimeout = function () {
|
||||
clearTimeout(this._stallAbortTimeout);
|
||||
// mark the timer as `null` so it is clear via introspection that the timeout is not scheduled
|
||||
delete this._stallAbortTimeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the next time a reconnect should occur (based on the last HTTP response received)
|
||||
* and starts a timeout handle to begin reconnecting after `self._connectInterval` passes.
|
||||
*
|
||||
* @return {Undefined}
|
||||
*/
|
||||
StreamingAPIConnection.prototype._scheduleReconnect = function () {
|
||||
var self = this;
|
||||
if (self.response && self.response.statusCode === 420) {
|
||||
// we are being rate limited
|
||||
// start with a 1 minute wait and double each attempt
|
||||
if (!self._connectInterval) {
|
||||
self._connectInterval = 60000;
|
||||
} else {
|
||||
self._connectInterval *= 2;
|
||||
}
|
||||
} else if (self.response && String(self.response.statusCode).charAt(0) === '5') {
|
||||
// twitter 5xx errors
|
||||
// start with a 5s wait, double each attempt up to 320s
|
||||
if (!self._connectInterval) {
|
||||
self._connectInterval = 5000;
|
||||
} else if (self._connectInterval < 320000) {
|
||||
self._connectInterval *= 2;
|
||||
} else {
|
||||
self._connectInterval = 320000;
|
||||
}
|
||||
} else {
|
||||
// we did not get an HTTP response from our last connection attempt.
|
||||
// DNS/TCP error, or a stall in the stream (and stall timer closed the connection)
|
||||
if (!self._usedFirstReconnect) {
|
||||
// first reconnection attempt on a valid connection should occur immediately
|
||||
self._connectInterval = 0;
|
||||
self._usedFirstReconnect = true;
|
||||
} else if (self._connectInterval < 16000) {
|
||||
// linearly increase delay by 250ms up to 16s
|
||||
self._connectInterval += 250;
|
||||
} else {
|
||||
// cap out reconnect interval at 16s
|
||||
self._connectInterval = 16000;
|
||||
}
|
||||
}
|
||||
|
||||
// schedule the reconnect
|
||||
self._scheduledReconnect = setTimeout(function () {
|
||||
self._startPersistentConnection();
|
||||
}, self._connectInterval);
|
||||
self.emit('reconnect', self.request, self.response, self._connectInterval);
|
||||
}
|
||||
|
||||
StreamingAPIConnection.prototype._setupParser = function () {
|
||||
var self = this
|
||||
self.parser = new Parser()
|
||||
|
||||
// handle twitter objects as they come in - emit the generic `message` event
|
||||
// along with the specific event corresponding to the message
|
||||
self.parser.on('element', function (msg) {
|
||||
self.emit('message', msg)
|
||||
|
||||
if (msg.delete) { self.emit('delete', msg) }
|
||||
else if (msg.disconnect) { self._handleDisconnect(msg) }
|
||||
else if (msg.limit) { self.emit('limit', msg) }
|
||||
else if (msg.scrub_geo) { self.emit('scrub_geo', msg) }
|
||||
else if (msg.warning) { self.emit('warning', msg) }
|
||||
else if (msg.status_withheld) { self.emit('status_withheld', msg) }
|
||||
else if (msg.user_withheld) { self.emit('user_withheld', msg) }
|
||||
else if (msg.friends || msg.friends_str) { self.emit('friends', msg) }
|
||||
else if (msg.direct_message) { self.emit('direct_message', msg) }
|
||||
else if (msg.event) {
|
||||
self.emit('user_event', msg)
|
||||
// reference: https://dev.twitter.com/docs/streaming-apis/messages#User_stream_messages
|
||||
var ev = msg.event
|
||||
|
||||
if (ev === 'blocked') { self.emit('blocked', msg) }
|
||||
else if (ev === 'unblocked') { self.emit('unblocked', msg) }
|
||||
else if (ev === 'favorite') { self.emit('favorite', msg) }
|
||||
else if (ev === 'unfavorite') { self.emit('unfavorite', msg) }
|
||||
else if (ev === 'follow') { self.emit('follow', msg) }
|
||||
else if (ev === 'unfollow') { self.emit('unfollow', msg) }
|
||||
else if (ev === 'user_update') { self.emit('user_update', msg) }
|
||||
else if (ev === 'list_created') { self.emit('list_created', msg) }
|
||||
else if (ev === 'list_destroyed') { self.emit('list_destroyed', msg) }
|
||||
else if (ev === 'list_updated') { self.emit('list_updated', msg) }
|
||||
else if (ev === 'list_member_added') { self.emit('list_member_added', msg) }
|
||||
else if (ev === 'list_member_removed') { self.emit('list_member_removed', msg) }
|
||||
else if (ev === 'list_user_subscribed') { self.emit('list_user_subscribed', msg) }
|
||||
else if (ev === 'list_user_unsubscribed') { self.emit('list_user_unsubscribed', msg) }
|
||||
else if (ev === 'quoted_tweet') { self.emit('quoted_tweet', msg) }
|
||||
else if (ev === 'favorited_retweet') { self.emit('favorited_retweet', msg) }
|
||||
else if (ev === 'retweeted_retweet') { self.emit('retweeted_retweet', msg) }
|
||||
else { self.emit('unknown_user_event', msg) }
|
||||
} else { self.emit('tweet', msg) }
|
||||
})
|
||||
|
||||
self.parser.on('error', function (err) {
|
||||
self.emit('parser-error', err)
|
||||
});
|
||||
self.parser.on('connection-limit-exceeded', function (err) {
|
||||
self.emit('error', err);
|
||||
})
|
||||
}
|
||||
|
||||
StreamingAPIConnection.prototype._handleDisconnect = function (twitterMsg) {
|
||||
this.emit('disconnect', twitterMsg);
|
||||
this.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call whenever an http request is about to be made to update
|
||||
* our local timestamp (used for Oauth) to be Twitter's server time.
|
||||
*
|
||||
*/
|
||||
StreamingAPIConnection.prototype._setOauthTimestamp = function () {
|
||||
var self = this;
|
||||
if (self.reqOpts.oauth) {
|
||||
var oauth_ts = Date.now() + self._twitter_time_minus_local_time_ms;
|
||||
self.reqOpts.oauth.timestamp = Math.floor(oauth_ts/1000).toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call whenever an http response is received from Twitter,
|
||||
* to set our local timestamp offset from Twitter's server time.
|
||||
* This is used to set the Oauth timestamp for our next http request
|
||||
* to Twitter (by calling _setOauthTimestamp).
|
||||
*
|
||||
* @param {http.IncomingResponse} resp http response received from Twitter.
|
||||
*/
|
||||
StreamingAPIConnection.prototype._updateOauthTimestampOffsetFromResponse = function (resp) {
|
||||
if (resp && resp.headers && resp.headers.date &&
|
||||
new Date(resp.headers.date).toString() !== 'Invalid Date'
|
||||
) {
|
||||
var twitterTimeMs = new Date(resp.headers.date).getTime()
|
||||
this._twitter_time_minus_local_time_ms = twitterTimeMs - Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StreamingAPIConnection
|
||||
485
node_modules/twit/lib/twitter.js
generated
vendored
Normal file
485
node_modules/twit/lib/twitter.js
generated
vendored
Normal file
@@ -0,0 +1,485 @@
|
||||
//
|
||||
// Twitter API Wrapper
|
||||
//
|
||||
var assert = require('assert');
|
||||
var Promise = require('bluebird');
|
||||
var request = require('request');
|
||||
var util = require('util');
|
||||
var endpoints = require('./endpoints');
|
||||
var FileUploader = require('./file_uploader');
|
||||
var helpers = require('./helpers');
|
||||
var StreamingAPIConnection = require('./streaming-api-connection');
|
||||
var STATUS_CODES_TO_ABORT_ON = require('./settings').STATUS_CODES_TO_ABORT_ON;
|
||||
|
||||
// config values required for app-only auth
|
||||
var required_for_app_auth = [
|
||||
'consumer_key',
|
||||
'consumer_secret'
|
||||
];
|
||||
|
||||
// config values required for user auth (superset of app-only auth)
|
||||
var required_for_user_auth = required_for_app_auth.concat([
|
||||
'access_token',
|
||||
'access_token_secret'
|
||||
]);
|
||||
|
||||
var FORMDATA_PATHS = [
|
||||
'media/upload',
|
||||
'account/update_profile_image',
|
||||
'account/update_profile_background_image',
|
||||
];
|
||||
|
||||
var JSONPAYLOAD_PATHS = [
|
||||
'media/metadata/create'
|
||||
]
|
||||
|
||||
//
|
||||
// Twitter
|
||||
//
|
||||
var Twitter = function (config) {
|
||||
if (!(this instanceof Twitter)) {
|
||||
return new Twitter(config);
|
||||
}
|
||||
|
||||
var self = this
|
||||
var credentials = {
|
||||
consumer_key : config.consumer_key,
|
||||
consumer_secret : config.consumer_secret,
|
||||
// access_token and access_token_secret only required for user auth
|
||||
access_token : config.access_token,
|
||||
access_token_secret : config.access_token_secret,
|
||||
// flag indicating whether requests should be made with application-only auth
|
||||
app_only_auth : config.app_only_auth,
|
||||
}
|
||||
|
||||
this._validateConfigOrThrow(config);
|
||||
this.config = config;
|
||||
this._twitter_time_minus_local_time_ms = 0;
|
||||
}
|
||||
|
||||
Twitter.prototype.get = function (path, params, callback) {
|
||||
return this.request('GET', path, params, callback)
|
||||
}
|
||||
|
||||
Twitter.prototype.post = function (path, params, callback) {
|
||||
return this.request('POST', path, params, callback)
|
||||
}
|
||||
|
||||
Twitter.prototype.request = function (method, path, params, callback) {
|
||||
var self = this;
|
||||
assert(method == 'GET' || method == 'POST');
|
||||
// if no `params` is specified but a callback is, use default params
|
||||
if (typeof params === 'function') {
|
||||
callback = params
|
||||
params = {}
|
||||
}
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
var _returnErrorToUser = function (err) {
|
||||
if (callback && typeof callback === 'function') {
|
||||
callback(err, null, null);
|
||||
}
|
||||
reject(err);
|
||||
}
|
||||
|
||||
self._buildReqOpts(method, path, params, false, function (err, reqOpts) {
|
||||
if (err) {
|
||||
_returnErrorToUser(err);
|
||||
return
|
||||
}
|
||||
|
||||
var twitOptions = (params && params.twit_options) || {};
|
||||
|
||||
process.nextTick(function () {
|
||||
// ensure all HTTP i/o occurs after the user has a chance to bind their event handlers
|
||||
self._doRestApiRequest(reqOpts, twitOptions, method, function (err, parsedBody, resp) {
|
||||
self._updateClockOffsetFromResponse(resp);
|
||||
|
||||
if (self.config.trusted_cert_fingerprints) {
|
||||
if (!resp.socket.authorized) {
|
||||
// The peer certificate was not signed by one of the authorized CA's.
|
||||
var authErrMsg = resp.socket.authorizationError.toString();
|
||||
var err = helpers.makeTwitError('The peer certificate was not signed; ' + authErrMsg);
|
||||
_returnErrorToUser(err);
|
||||
return;
|
||||
}
|
||||
var fingerprint = resp.socket.getPeerCertificate().fingerprint;
|
||||
var trustedFingerprints = self.config.trusted_cert_fingerprints;
|
||||
if (trustedFingerprints.indexOf(fingerprint) === -1) {
|
||||
var errMsg = util.format('Certificate untrusted. Trusted fingerprints are: %s. Got fingerprint: %s.',
|
||||
trustedFingerprints.join(','), fingerprint);
|
||||
var err = new Error(errMsg);
|
||||
_returnErrorToUser(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (callback && typeof callback === 'function') {
|
||||
callback(err, parsedBody, resp);
|
||||
}
|
||||
|
||||
resolve({ data: parsedBody, resp: resp });
|
||||
return;
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file to Twitter via the POST media/upload (chunked) API.
|
||||
* Use this as an easier alternative to doing the INIT/APPEND/FINALIZE commands yourself.
|
||||
* Returns the response from the FINALIZE command, or if an error occurs along the way,
|
||||
* the first argument to `cb` will be populated with a non-null Error.
|
||||
*
|
||||
*
|
||||
* `params` is an Object of the form:
|
||||
* {
|
||||
* file_path: String // Absolute path of file to be uploaded.
|
||||
* }
|
||||
*
|
||||
* @param {Object} params options object (described above).
|
||||
* @param {cb} cb callback of the form: function (err, bodyObj, resp)
|
||||
*/
|
||||
Twitter.prototype.postMediaChunked = function (params, cb) {
|
||||
var self = this;
|
||||
try {
|
||||
var fileUploader = new FileUploader(params, self);
|
||||
} catch(err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
fileUploader.upload(cb);
|
||||
}
|
||||
|
||||
Twitter.prototype._updateClockOffsetFromResponse = function (resp) {
|
||||
var self = this;
|
||||
if (resp && resp.headers && resp.headers.date &&
|
||||
new Date(resp.headers.date).toString() !== 'Invalid Date'
|
||||
) {
|
||||
var twitterTimeMs = new Date(resp.headers.date).getTime()
|
||||
self._twitter_time_minus_local_time_ms = twitterTimeMs - Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns an options object ready to pass to `request()`
|
||||
* @param {String} method "GET" or "POST"
|
||||
* @param {String} path REST API resource uri (eg. "statuses/destroy/:id")
|
||||
* @param {Object} params user's params object
|
||||
* @param {Boolean} isStreaming Flag indicating if it's a request to the Streaming API (different endpoint)
|
||||
* @returns {Undefined}
|
||||
*
|
||||
* Calls `callback` with Error, Object where Object is an options object ready to pass to `request()`.
|
||||
*
|
||||
* Returns error raised (if any) by `helpers.moveParamsIntoPath()`
|
||||
*/
|
||||
Twitter.prototype._buildReqOpts = function (method, path, params, isStreaming, callback) {
|
||||
var self = this
|
||||
if (!params) {
|
||||
params = {}
|
||||
}
|
||||
// clone `params` object so we can modify it without modifying the user's reference
|
||||
var paramsClone = JSON.parse(JSON.stringify(params))
|
||||
// convert any arrays in `paramsClone` to comma-seperated strings
|
||||
var finalParams = this.normalizeParams(paramsClone)
|
||||
delete finalParams.twit_options
|
||||
|
||||
// the options object passed to `request` used to perform the HTTP request
|
||||
var reqOpts = {
|
||||
headers: {
|
||||
'Accept': '*/*',
|
||||
'User-Agent': 'twit-client'
|
||||
},
|
||||
gzip: true,
|
||||
encoding: null,
|
||||
}
|
||||
|
||||
if (typeof self.config.timeout_ms !== 'undefined') {
|
||||
reqOpts.timeout = self.config.timeout_ms;
|
||||
}
|
||||
|
||||
try {
|
||||
// finalize the `path` value by building it using user-supplied params
|
||||
path = helpers.moveParamsIntoPath(finalParams, path)
|
||||
} catch (e) {
|
||||
callback(e, null, null)
|
||||
return
|
||||
}
|
||||
|
||||
if (isStreaming) {
|
||||
// This is a Streaming API request.
|
||||
|
||||
var stream_endpoint_map = {
|
||||
user: endpoints.USER_STREAM,
|
||||
site: endpoints.SITE_STREAM
|
||||
}
|
||||
var endpoint = stream_endpoint_map[path] || endpoints.PUB_STREAM
|
||||
reqOpts.url = endpoint + path + '.json'
|
||||
} else {
|
||||
// This is a REST API request.
|
||||
|
||||
if (path.indexOf('media/') !== -1) {
|
||||
// For media/upload, use a different endpoint.
|
||||
reqOpts.url = endpoints.MEDIA_UPLOAD + path + '.json';
|
||||
} else {
|
||||
reqOpts.url = endpoints.REST_ROOT + path + '.json';
|
||||
}
|
||||
|
||||
if (FORMDATA_PATHS.indexOf(path) !== -1) {
|
||||
reqOpts.headers['Content-type'] = 'multipart/form-data';
|
||||
reqOpts.form = finalParams;
|
||||
// set finalParams to empty object so we don't append a query string
|
||||
// of the params
|
||||
finalParams = {};
|
||||
} else if (JSONPAYLOAD_PATHS.indexOf(path) !== -1) {
|
||||
reqOpts.headers['Content-type'] = 'application/json';
|
||||
reqOpts.json = true;
|
||||
reqOpts.body = finalParams;
|
||||
// as above, to avoid appending query string for body params
|
||||
finalParams = {};
|
||||
} else {
|
||||
reqOpts.headers['Content-type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(finalParams).length) {
|
||||
// not all of the user's parameters were used to build the request path
|
||||
// add them as a query string
|
||||
var qs = helpers.makeQueryString(finalParams)
|
||||
reqOpts.url += '?' + qs
|
||||
}
|
||||
|
||||
if (!self.config.app_only_auth) {
|
||||
// with user auth, we can just pass an oauth object to requests
|
||||
// to have the request signed
|
||||
var oauth_ts = Date.now() + self._twitter_time_minus_local_time_ms;
|
||||
|
||||
reqOpts.oauth = {
|
||||
consumer_key: self.config.consumer_key,
|
||||
consumer_secret: self.config.consumer_secret,
|
||||
token: self.config.access_token,
|
||||
token_secret: self.config.access_token_secret,
|
||||
timestamp: Math.floor(oauth_ts/1000).toString(),
|
||||
}
|
||||
|
||||
callback(null, reqOpts);
|
||||
return;
|
||||
} else {
|
||||
// we're using app-only auth, so we need to ensure we have a bearer token
|
||||
// Once we have a bearer token, add the Authorization header and return the fully qualified `reqOpts`.
|
||||
self._getBearerToken(function (err, bearerToken) {
|
||||
if (err) {
|
||||
callback(err, null)
|
||||
return
|
||||
}
|
||||
|
||||
reqOpts.headers['Authorization'] = 'Bearer ' + bearerToken;
|
||||
callback(null, reqOpts)
|
||||
return
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make HTTP request to Twitter REST API.
|
||||
* @param {Object} reqOpts options object passed to `request()`
|
||||
* @param {Object} twitOptions
|
||||
* @param {String} method "GET" or "POST"
|
||||
* @param {Function} callback user's callback
|
||||
* @return {Undefined}
|
||||
*/
|
||||
Twitter.prototype._doRestApiRequest = function (reqOpts, twitOptions, method, callback) {
|
||||
var request_method = request[method.toLowerCase()];
|
||||
var req = request_method(reqOpts);
|
||||
|
||||
var body = '';
|
||||
var response = null;
|
||||
|
||||
var onRequestComplete = function () {
|
||||
if (body !== '') {
|
||||
try {
|
||||
body = JSON.parse(body)
|
||||
} catch (jsonDecodeError) {
|
||||
// there was no transport-level error, but a JSON object could not be decoded from the request body
|
||||
// surface this to the caller
|
||||
var err = helpers.makeTwitError('JSON decode error: Twitter HTTP response body was not valid JSON')
|
||||
err.statusCode = response ? response.statusCode: null;
|
||||
err.allErrors.concat({error: jsonDecodeError.toString()})
|
||||
callback(err, body, response);
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof body === 'object' && (body.error || body.errors)) {
|
||||
// we got a Twitter API-level error response
|
||||
// place the errors in the HTTP response body into the Error object and pass control to caller
|
||||
var err = helpers.makeTwitError('Twitter API Error')
|
||||
err.statusCode = response ? response.statusCode: null;
|
||||
helpers.attachBodyInfoToError(err, body);
|
||||
callback(err, body, response);
|
||||
return
|
||||
}
|
||||
|
||||
// success case - no errors in HTTP response body
|
||||
callback(err, body, response)
|
||||
}
|
||||
|
||||
req.on('response', function (res) {
|
||||
response = res
|
||||
// read data from `request` object which contains the decompressed HTTP response body,
|
||||
// `response` is the unmodified http.IncomingMessage object which may contain compressed data
|
||||
req.on('data', function (chunk) {
|
||||
body += chunk.toString('utf8')
|
||||
})
|
||||
// we're done reading the response
|
||||
req.on('end', function () {
|
||||
onRequestComplete()
|
||||
})
|
||||
})
|
||||
|
||||
req.on('error', function (err) {
|
||||
// transport-level error occurred - likely a socket error
|
||||
if (twitOptions.retry &&
|
||||
STATUS_CODES_TO_ABORT_ON.indexOf(err.statusCode) !== -1
|
||||
) {
|
||||
// retry the request since retries were specified and we got a status code we should retry on
|
||||
self.request(method, path, params, callback);
|
||||
return;
|
||||
} else {
|
||||
// pass the transport-level error to the caller
|
||||
err.statusCode = null
|
||||
err.code = null
|
||||
err.allErrors = [];
|
||||
helpers.attachBodyInfoToError(err, body)
|
||||
callback(err, body, response);
|
||||
return;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates/starts a connection object that stays connected to Twitter's servers
|
||||
* using Twitter's rules.
|
||||
*
|
||||
* @param {String} path Resource path to connect to (eg. "statuses/sample")
|
||||
* @param {Object} params user's params object
|
||||
* @return {StreamingAPIConnection} [description]
|
||||
*/
|
||||
Twitter.prototype.stream = function (path, params) {
|
||||
var self = this;
|
||||
var twitOptions = (params && params.twit_options) || {};
|
||||
|
||||
var streamingConnection = new StreamingAPIConnection()
|
||||
self._buildReqOpts('POST', path, params, true, function (err, reqOpts) {
|
||||
if (err) {
|
||||
// we can get an error if we fail to obtain a bearer token or construct reqOpts
|
||||
// surface this on the streamingConnection instance (where a user may register their error handler)
|
||||
streamingConnection.emit('error', err)
|
||||
return
|
||||
}
|
||||
// set the properties required to start the connection
|
||||
streamingConnection.reqOpts = reqOpts
|
||||
streamingConnection.twitOptions = twitOptions
|
||||
|
||||
process.nextTick(function () {
|
||||
streamingConnection.start()
|
||||
})
|
||||
})
|
||||
|
||||
return streamingConnection
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets bearer token from cached reference on `self`, or fetches a new one and sets it on `self`.
|
||||
*
|
||||
* @param {Function} callback Function to invoke with (Error, bearerToken)
|
||||
* @return {Undefined}
|
||||
*/
|
||||
Twitter.prototype._getBearerToken = function (callback) {
|
||||
var self = this;
|
||||
if (self._bearerToken) {
|
||||
return callback(null, self._bearerToken)
|
||||
}
|
||||
|
||||
helpers.getBearerToken(self.config.consumer_key, self.config.consumer_secret,
|
||||
function (err, bearerToken) {
|
||||
if (err) {
|
||||
// return the fully-qualified Twit Error object to caller
|
||||
callback(err, null);
|
||||
return;
|
||||
}
|
||||
self._bearerToken = bearerToken;
|
||||
callback(null, self._bearerToken);
|
||||
return;
|
||||
})
|
||||
}
|
||||
|
||||
Twitter.prototype.normalizeParams = function (params) {
|
||||
var normalized = params
|
||||
if (params && typeof params === 'object') {
|
||||
Object.keys(params).forEach(function (key) {
|
||||
var value = params[key]
|
||||
// replace any arrays in `params` with comma-separated string
|
||||
if (Array.isArray(value))
|
||||
normalized[key] = value.join(',')
|
||||
})
|
||||
} else if (!params) {
|
||||
normalized = {}
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
Twitter.prototype.setAuth = function (auth) {
|
||||
var self = this
|
||||
var configKeys = [
|
||||
'consumer_key',
|
||||
'consumer_secret',
|
||||
'access_token',
|
||||
'access_token_secret'
|
||||
];
|
||||
|
||||
// update config
|
||||
configKeys.forEach(function (k) {
|
||||
if (auth[k]) {
|
||||
self.config[k] = auth[k]
|
||||
}
|
||||
})
|
||||
this._validateConfigOrThrow(self.config);
|
||||
}
|
||||
|
||||
Twitter.prototype.getAuth = function () {
|
||||
return this.config
|
||||
}
|
||||
|
||||
//
|
||||
// Check that the required auth credentials are present in `config`.
|
||||
// @param {Object} config Object containing credentials for REST API auth
|
||||
//
|
||||
Twitter.prototype._validateConfigOrThrow = function (config) {
|
||||
//check config for proper format
|
||||
if (typeof config !== 'object') {
|
||||
throw new TypeError('config must be object, got ' + typeof config)
|
||||
}
|
||||
|
||||
if (typeof config.timeout_ms !== 'undefined' && isNaN(Number(config.timeout_ms))) {
|
||||
throw new TypeError('Twit config `timeout_ms` must be a Number. Got: ' + config.timeout_ms + '.');
|
||||
}
|
||||
|
||||
if (config.app_only_auth) {
|
||||
var auth_type = 'app-only auth'
|
||||
var required_keys = required_for_app_auth
|
||||
} else {
|
||||
var auth_type = 'user auth'
|
||||
var required_keys = required_for_user_auth
|
||||
}
|
||||
|
||||
required_keys.forEach(function (req_key) {
|
||||
if (!config[req_key]) {
|
||||
var err_msg = util.format('Twit config must include `%s` when using %s.', req_key, auth_type)
|
||||
throw new Error(err_msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = Twitter
|
||||
105
node_modules/twit/package.json
generated
vendored
Normal file
105
node_modules/twit/package.json
generated
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"_args": [
|
||||
[
|
||||
{
|
||||
"name": "twit",
|
||||
"raw": "twit",
|
||||
"rawSpec": "",
|
||||
"scope": null,
|
||||
"spec": "latest",
|
||||
"type": "tag"
|
||||
},
|
||||
"C:\\Users\\talor\\bots\\polibug"
|
||||
]
|
||||
],
|
||||
"_from": "twit@latest",
|
||||
"_id": "twit@2.2.4",
|
||||
"_inCache": true,
|
||||
"_installable": true,
|
||||
"_location": "/twit",
|
||||
"_nodeVersion": "0.12.0",
|
||||
"_npmOperationalInternal": {
|
||||
"host": "packages-12-west.internal.npmjs.com",
|
||||
"tmp": "tmp/twit-2.2.4.tgz_1462157237561_0.11102098692208529"
|
||||
},
|
||||
"_npmUser": {
|
||||
"email": "tolgatezel11@gmail.com",
|
||||
"name": "ttezel"
|
||||
},
|
||||
"_npmVersion": "2.5.1",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"name": "twit",
|
||||
"raw": "twit",
|
||||
"rawSpec": "",
|
||||
"scope": null,
|
||||
"spec": "latest",
|
||||
"type": "tag"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"#USER",
|
||||
"/"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/twit/-/twit-2.2.4.tgz",
|
||||
"_shasum": "811e10092eb30ded476e21689dd8a90ee63963cb",
|
||||
"_shrinkwrap": null,
|
||||
"_spec": "twit",
|
||||
"_where": "C:\\Users\\talor\\bots\\polibug",
|
||||
"author": {
|
||||
"name": "Tolga Tezel"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/ttezel/twit/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"bluebird": "^3.1.5",
|
||||
"mime": "^1.3.4",
|
||||
"request": "2.58.0"
|
||||
},
|
||||
"description": "Twitter API client for node (REST & Streaming)",
|
||||
"devDependencies": {
|
||||
"async": "0.2.9",
|
||||
"colors": "0.6.x",
|
||||
"commander": "2.6.0",
|
||||
"mocha": "2.1.0",
|
||||
"rewire": "2.3.4",
|
||||
"sinon": "1.15.4"
|
||||
},
|
||||
"directories": {},
|
||||
"dist": {
|
||||
"shasum": "811e10092eb30ded476e21689dd8a90ee63963cb",
|
||||
"tarball": "https://registry.npmjs.org/twit/-/twit-2.2.4.tgz"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"gitHead": "3230bc88906cfcb2608fbee360cee98729a6580c",
|
||||
"homepage": "https://github.com/ttezel/twit",
|
||||
"keywords": [
|
||||
"twitter",
|
||||
"api",
|
||||
"rest",
|
||||
"stream",
|
||||
"streaming",
|
||||
"oauth"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "./lib/twitter",
|
||||
"maintainers": [
|
||||
{
|
||||
"email": "tolgatezel11@gmail.com",
|
||||
"name": "ttezel"
|
||||
}
|
||||
],
|
||||
"name": "twit",
|
||||
"optionalDependencies": {},
|
||||
"readme": "ERROR: No README data found!",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+ssh://git@github.com/ttezel/twit.git"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "mocha tests/* -t 70000 -R spec --bail --globals domain,_events,_maxListeners"
|
||||
},
|
||||
"version": "2.2.4"
|
||||
}
|
||||
46
node_modules/twit/tests/helpers.js
generated
vendored
Normal file
46
node_modules/twit/tests/helpers.js
generated
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var stream = require('stream');
|
||||
var util = require('util');
|
||||
|
||||
// Used to stub out calls to `request`.
|
||||
exports.FakeRequest = function () {
|
||||
EventEmitter.call(this)
|
||||
}
|
||||
util.inherits(exports.FakeRequest, EventEmitter)
|
||||
exports.FakeRequest.prototype.destroy = function () {
|
||||
|
||||
}
|
||||
|
||||
// Used to stub out the http.IncomingMessage object emitted by the "response" event on `request`.
|
||||
exports.FakeResponse = function (statusCode, body) {
|
||||
if (!body) {
|
||||
body = '';
|
||||
}
|
||||
this.statusCode = statusCode;
|
||||
stream.Readable.call(this);
|
||||
this.push(body);
|
||||
this.push(null);
|
||||
}
|
||||
util.inherits(exports.FakeResponse, stream.Readable);
|
||||
|
||||
exports.FakeResponse.prototype._read = function () {
|
||||
|
||||
}
|
||||
exports.FakeResponse.prototype.destroy = function () {
|
||||
|
||||
}
|
||||
|
||||
exports.generateRandomString = function generateRandomString (length) {
|
||||
var length = length || 10
|
||||
var ret = ''
|
||||
var alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
||||
for (var i = 0; i < length; i++) {
|
||||
// use an easy set of unicode as an alphabet - twitter won't reformat them
|
||||
// which makes testing easier
|
||||
ret += alphabet[Math.floor(Math.random()*alphabet.length)]
|
||||
}
|
||||
|
||||
ret = encodeURI(ret)
|
||||
|
||||
return ret
|
||||
}
|
||||
BIN
node_modules/twit/tests/img/bigbird.jpg
generated
vendored
Normal file
BIN
node_modules/twit/tests/img/bigbird.jpg
generated
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
BIN
node_modules/twit/tests/img/cutebird.png
generated
vendored
Normal file
BIN
node_modules/twit/tests/img/cutebird.png
generated
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
node_modules/twit/tests/img/snoopy-animated.gif
generated
vendored
Normal file
BIN
node_modules/twit/tests/img/snoopy-animated.gif
generated
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 693 B |
BIN
node_modules/twit/tests/img/twitterbird.gif
generated
vendored
Normal file
BIN
node_modules/twit/tests/img/twitterbird.gif
generated
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
74
node_modules/twit/tests/multiple-conn.js
generated
vendored
Normal file
74
node_modules/twit/tests/multiple-conn.js
generated
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
var assert = require('assert');
|
||||
|
||||
var Twit = require('../lib/twitter');
|
||||
var config1 = require('../config1');
|
||||
var colors = require('colors');
|
||||
var restTest = require('./rest.js');
|
||||
|
||||
/*
|
||||
Don't run these tests often otherwise Twitter will rate limit you
|
||||
*/
|
||||
|
||||
describe.skip('multiple connections', function () {
|
||||
it('results in one of the streams closing', function (done) {
|
||||
var twit = new Twit(config1);
|
||||
|
||||
var streams = [
|
||||
twit.stream('statuses/sample'),
|
||||
twit.stream('statuses/sample'),
|
||||
twit.stream('statuses/sample'),
|
||||
];
|
||||
|
||||
streams.forEach(function (stream, i) {
|
||||
stream.on('disconnect', function (disconnect) {
|
||||
console.log('Disconect for stream', i)
|
||||
assert.equal(typeof disconnect, 'object');
|
||||
done();
|
||||
});
|
||||
|
||||
stream.on('error', function (errMsg) {
|
||||
console.log('error for stream', i, errMsg)
|
||||
})
|
||||
|
||||
stream.on('tweet', function (t) {
|
||||
console.log(i)
|
||||
})
|
||||
|
||||
stream.on('connected', function () {
|
||||
console.log('Stream', i, 'connected.')
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('Managing multiple streams legally', function () {
|
||||
this.timeout(60000);
|
||||
it('updating track keywords without losing data', function (done) {
|
||||
var twit = new Twit(config1);
|
||||
var stream1 = twit.stream('statuses/filter', { track: ['#no'] });
|
||||
|
||||
stream1.once('tweet', function (tweet) {
|
||||
console.log('got tweet from first stream')
|
||||
restTest.checkTweet(tweet);
|
||||
restTest.assertTweetHasText(tweet, '#no');
|
||||
|
||||
// update our track list and initiate a new connection
|
||||
var stream2 = twit.stream('statuses/filter', { track: ['#fun'] });
|
||||
|
||||
stream2.once('connected', function (res) {
|
||||
console.log('second stream connected')
|
||||
// stop the first stream immediately
|
||||
stream1.stop();
|
||||
assert.equal(res.statusCode, 200)
|
||||
|
||||
stream2.once('tweet', function (tweet) {
|
||||
restTest.checkTweet(tweet);
|
||||
|
||||
restTest.assertTweetHasText(tweet, '#fun');
|
||||
return done();
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
823
node_modules/twit/tests/rest.js
generated
vendored
Normal file
823
node_modules/twit/tests/rest.js
generated
vendored
Normal file
@@ -0,0 +1,823 @@
|
||||
var assert = require('assert')
|
||||
, EventEmitter = require('events').EventEmitter
|
||||
, fs = require('fs')
|
||||
, sinon = require('sinon')
|
||||
, Twit = require('../lib/twitter')
|
||||
, config1 = require('../config1')
|
||||
, config2 = require('../config2')
|
||||
, helpers = require('./helpers')
|
||||
, util = require('util')
|
||||
, async = require('async')
|
||||
|
||||
describe('REST API', function () {
|
||||
var twit = null
|
||||
|
||||
before(function () {
|
||||
twit = new Twit(config1);
|
||||
})
|
||||
|
||||
it('GET `account/verify_credentials`', function (done) {
|
||||
twit.get('account/verify_credentials', function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
assert.notEqual(reply.followers_count, undefined)
|
||||
assert.notEqual(reply.friends_count, undefined)
|
||||
assert.ok(reply.id_str)
|
||||
|
||||
checkResponse(response)
|
||||
|
||||
assert(response.headers['x-rate-limit-limit'])
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('GET `account/verify_credentials` using promise API only', function (done) {
|
||||
twit
|
||||
.get('account/verify_credentials', { skip_status: true })
|
||||
.catch(function (err) {
|
||||
console.log('catch err', err.stack)
|
||||
})
|
||||
.then(function (result) {
|
||||
checkReply(null, result.data)
|
||||
assert.notEqual(result.data.followers_count, undefined)
|
||||
assert.notEqual(result.data.friends_count, undefined)
|
||||
assert.ok(result.data.id_str)
|
||||
checkResponse(result.resp)
|
||||
assert(result.resp.headers['x-rate-limit-limit'])
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('GET `account/verify_credentials` using promise API AND callback API', function (done) {
|
||||
var i = 0;
|
||||
|
||||
var _checkDataAndResp = function (data, resp) {
|
||||
checkReply(null, data)
|
||||
assert.notEqual(data.followers_count, undefined)
|
||||
assert.notEqual(data.friends_count, undefined)
|
||||
assert.ok(data.id_str)
|
||||
checkResponse(resp)
|
||||
assert(resp.headers['x-rate-limit-limit'])
|
||||
|
||||
i++;
|
||||
if (i == 2) {
|
||||
done()
|
||||
}
|
||||
}
|
||||
|
||||
var get = twit.get('account/verify_credentials', function (err, data, resp) {
|
||||
assert(!err, err);
|
||||
_checkDataAndResp(data, resp);
|
||||
});
|
||||
get.catch(function (err) {
|
||||
console.log('Got error:', err.stack)
|
||||
});
|
||||
get.then(function (result) {
|
||||
_checkDataAndResp(result.data, result.resp);
|
||||
});
|
||||
})
|
||||
|
||||
it('POST `account/update_profile`', function (done) {
|
||||
twit.post('account/update_profile', function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
console.log('\nscreen name:', reply.screen_name)
|
||||
checkResponse(response)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('POST `statuses/update` and POST `statuses/destroy:id`', function (done) {
|
||||
var tweetId = null
|
||||
|
||||
var params = {
|
||||
status: '@tolga_tezel tweeting using github.com/ttezel/twit. ' + helpers.generateRandomString(7)
|
||||
}
|
||||
twit.post('statuses/update', params, function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
console.log('\ntweeted:', reply.text)
|
||||
|
||||
tweetId = reply.id_str
|
||||
assert(tweetId)
|
||||
checkResponse(response)
|
||||
|
||||
var deleteParams = { id: tweetId }
|
||||
// Try up to 2 times to delete the tweet.
|
||||
// Even after a 200 response to statuses/update is returned, a delete might 404 so we retry.
|
||||
exports.req_with_retries(twit, 2, 'post', 'statuses/destroy/:id', deleteParams, [404], function (err, body, response) {
|
||||
checkReply(err, body)
|
||||
checkTweet(body)
|
||||
checkResponse(response)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('POST `statuses/update` with characters requiring escaping', function (done) {
|
||||
var params = { status: '@tolga_tezel tweeting using github.com/ttezel/twit :) !' + helpers.generateRandomString(15) }
|
||||
|
||||
twit.post('statuses/update', params, function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
|
||||
console.log('\ntweeted:', reply.text)
|
||||
|
||||
checkResponse(response)
|
||||
|
||||
var text = reply.text
|
||||
|
||||
assert(reply.id_str)
|
||||
|
||||
twit.post('statuses/destroy/:id', { id: reply.id_str }, function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
checkTweet(reply)
|
||||
assert.equal(reply.text, text)
|
||||
|
||||
checkResponse(response)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('POST `statuses/update` with \'Hi!!\' works', function (done) {
|
||||
var params = { status: 'Hi!!' }
|
||||
|
||||
twit.post('statuses/update', params, function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
|
||||
console.log('\ntweeted:', reply.text)
|
||||
|
||||
checkResponse(response)
|
||||
|
||||
var text = reply.text
|
||||
|
||||
var destroyRoute = 'statuses/destroy/'+reply.id_str
|
||||
|
||||
twit.post(destroyRoute, function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
checkTweet(reply)
|
||||
assert.equal(reply.text, text)
|
||||
|
||||
checkResponse(response)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('GET `statuses/home_timeline`', function (done) {
|
||||
twit.get('statuses/home_timeline', function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
checkTweet(reply[0])
|
||||
|
||||
checkResponse(response)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('GET `statuses/mentions_timeline`', function (done) {
|
||||
twit.get('statuses/mentions_timeline', function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
checkTweet(reply[0])
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('GET `statuses/user_timeline`', function (done) {
|
||||
var params = {
|
||||
screen_name: 'tolga_tezel'
|
||||
}
|
||||
|
||||
twit.get('statuses/user_timeline', params, function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
checkTweet(reply[0])
|
||||
|
||||
checkResponse(response)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('GET `search/tweets` { q: "a", since_id: 12345 }', function (done) {
|
||||
var params = { q: 'a', since_id: 12345 }
|
||||
twit.get('search/tweets', params, function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
assert.ok(reply.statuses)
|
||||
checkTweet(reply.statuses[0])
|
||||
|
||||
checkResponse(response)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('GET `search/tweets` { q: "fun since:2011-11-11" }', function (done) {
|
||||
var params = { q: 'fun since:2011-11-11', count: 100 }
|
||||
twit.get('search/tweets', params, function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
assert.ok(reply.statuses)
|
||||
checkTweet(reply.statuses[0])
|
||||
|
||||
console.log('\nnumber of fun statuses:', reply.statuses.length)
|
||||
|
||||
checkResponse(response)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('GET `search/tweets`, using `q` array', function (done) {
|
||||
var params = {
|
||||
q: [ 'banana', 'mango', 'peach' ]
|
||||
}
|
||||
|
||||
twit.get('search/tweets', params, function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
assert.ok(reply.statuses)
|
||||
checkTweet(reply.statuses[0])
|
||||
|
||||
checkResponse(response)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('GET `search/tweets` with count set to 100', function (done) {
|
||||
var params = {
|
||||
q: 'happy',
|
||||
count: 100
|
||||
}
|
||||
|
||||
twit.get('search/tweets', params, function (err, reply, res) {
|
||||
checkReply(err, reply)
|
||||
console.log('\nnumber of tweets from search:', reply.statuses.length)
|
||||
// twitter won't always send back 100 tweets if we ask for 100,
|
||||
// but make sure it's close to 100
|
||||
assert(reply.statuses.length > 95)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('GET `search/tweets` with geocode', function (done) {
|
||||
var params = {
|
||||
q: 'apple', geocode: [ '37.781157', '-122.398720', '1mi' ]
|
||||
}
|
||||
|
||||
twit.get('search/tweets', params, function (err, reply) {
|
||||
checkReply(err, reply)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('GET `direct_messages`', function (done) {
|
||||
twit.get('direct_messages', function (err, reply, response) {
|
||||
checkResponse(response)
|
||||
checkReply(err, reply)
|
||||
assert.ok(Array.isArray(reply))
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('GET `followers/ids`', function (done) {
|
||||
twit.get('followers/ids', function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
assert.ok(Array.isArray(reply.ids))
|
||||
|
||||
checkResponse(response)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('GET `followers/ids` of screen_name tolga_tezel', function (done) {
|
||||
twit.get('followers/ids', { screen_name: 'tolga_tezel' }, function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
assert.ok(Array.isArray(reply.ids))
|
||||
|
||||
checkResponse(response)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('POST `statuses/retweet`', function (done) {
|
||||
// search for a tweet to retweet
|
||||
twit.get('search/tweets', { q: 'apple' }, function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
assert.ok(reply.statuses)
|
||||
|
||||
var tweet = reply.statuses[0]
|
||||
checkTweet(tweet)
|
||||
|
||||
var tweetId = tweet.id_str
|
||||
assert(tweetId)
|
||||
|
||||
twit.post('statuses/retweet/'+tweetId, function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
|
||||
var retweetId = reply.id_str
|
||||
assert(retweetId)
|
||||
|
||||
twit.post('statuses/destroy/'+retweetId, function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// 1.1.8 usage
|
||||
it('POST `statuses/retweet/:id` without `id` in params returns error', function (done) {
|
||||
twit.post('statuses/retweet/:id', function (err, reply, response) {
|
||||
assert(err)
|
||||
assert.equal(err.message, 'Twit: Params object is missing a required parameter for this request: `id`')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
// 1.1.8 usage
|
||||
it('POST `statuses/retweet/:id`', function (done) {
|
||||
// search for a tweet to retweet
|
||||
twit.get('search/tweets', { q: 'banana' }, function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
assert.ok(reply.statuses)
|
||||
|
||||
var tweet = reply.statuses[0]
|
||||
checkTweet(tweet)
|
||||
|
||||
var tweetId = tweet.id_str
|
||||
assert(tweetId)
|
||||
|
||||
twit.post('statuses/retweet/:id', { id: tweetId }, function (err, reply) {
|
||||
checkReply(err, reply)
|
||||
|
||||
var retweetId = reply.id_str
|
||||
assert(retweetId)
|
||||
|
||||
twit.post('statuses/destroy/:id', { id: retweetId }, function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// 1.1.8 usage
|
||||
// skip for now since this API call is having problems on Twitter's side (404)
|
||||
it.skip('GET `users/suggestions/:slug`', function (done) {
|
||||
twit.get('users/suggestions/:slug', { slug: 'funny' }, function (err, reply, res) {
|
||||
checkReply(err, reply)
|
||||
assert.equal(reply.slug, 'funny')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
// 1.1.8 usage
|
||||
// skip for now since this API call is having problems on Twitter's side (404)
|
||||
it.skip('GET `users/suggestions/:slug/members`', function (done) {
|
||||
twit.get('users/suggestions/:slug/members', { slug: 'funny' }, function (err, reply, res) {
|
||||
checkReply(err, reply)
|
||||
|
||||
assert(reply[0].id_str)
|
||||
assert(reply[0].screen_name)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
// 1.1.8 usage
|
||||
it('GET `geo/id/:place_id`', function (done) {
|
||||
var placeId = 'df51dec6f4ee2b2c'
|
||||
|
||||
twit.get('geo/id/:place_id', { place_id: placeId }, function (err, reply, res) {
|
||||
checkReply(err, reply)
|
||||
|
||||
assert(reply.country)
|
||||
assert(reply.bounding_box)
|
||||
assert.equal(reply.id, placeId)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('POST `direct_messages/new`', function (done) {
|
||||
var dmId
|
||||
|
||||
async.series({
|
||||
postDm: function (next) {
|
||||
|
||||
var dmParams = {
|
||||
screen_name: 'tolga_tezel',
|
||||
text: 'hey this is a direct message from twit! :) ' + helpers.generateRandomString(15)
|
||||
}
|
||||
// post a direct message from the sender's account
|
||||
twit.post('direct_messages/new', dmParams, function (err, reply) {
|
||||
assert(!err, err)
|
||||
assert(reply)
|
||||
|
||||
dmId = reply.id_str
|
||||
|
||||
exports.checkDm(reply)
|
||||
|
||||
assert.equal(reply.text, dmParams.text)
|
||||
assert(dmId)
|
||||
|
||||
return next()
|
||||
})
|
||||
},
|
||||
deleteDm: function (next) {
|
||||
twit.post('direct_messages/destroy', { id: dmId }, function (err, reply) {
|
||||
assert(!err, err)
|
||||
exports.checkDm(reply)
|
||||
assert.equal(reply.id, dmId)
|
||||
|
||||
return next()
|
||||
})
|
||||
}
|
||||
}, done);
|
||||
})
|
||||
|
||||
describe('Media Upload', function () {
|
||||
var twit = null
|
||||
|
||||
before(function () {
|
||||
twit = new Twit(config1)
|
||||
})
|
||||
|
||||
it('POST media/upload with png', function (done) {
|
||||
var b64content = fs.readFileSync(__dirname + '/img/cutebird.png', { encoding: 'base64' })
|
||||
|
||||
twit.post('media/upload', { media_data: b64content }, function (err, data, response) {
|
||||
assert.equal(response.statusCode, 200)
|
||||
assert(!err, err)
|
||||
exports.checkMediaUpload(data)
|
||||
assert(data.image.image_type == 'image/png' || data.image.image_type == 'image\/png')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('POST media/upload with JPG', function (done) {
|
||||
var b64content = fs.readFileSync(__dirname + '/img/bigbird.jpg', { encoding: 'base64' })
|
||||
|
||||
twit.post('media/upload', { media_data: b64content }, function (err, data, response) {
|
||||
assert(!err, err)
|
||||
exports.checkMediaUpload(data)
|
||||
assert.equal(data.image.image_type, 'image/jpeg')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('POST media/upload with static GIF', function (done) {
|
||||
var b64content = fs.readFileSync(__dirname + '/img/twitterbird.gif', { encoding: 'base64' })
|
||||
|
||||
twit.post('media/upload', { media_data: b64content }, function (err, data, response) {
|
||||
assert(!err, err)
|
||||
exports.checkMediaUpload(data)
|
||||
assert.equal(data.image.image_type, 'image/gif')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('POST media/upload with animated GIF using `media_data` parameter', function (done) {
|
||||
var b64content = fs.readFileSync(__dirname + '/img/snoopy-animated.gif', { encoding: 'base64' })
|
||||
|
||||
twit.post('media/upload', { media_data: b64content }, function (err, data, response) {
|
||||
assert(!err, err)
|
||||
exports.checkMediaUpload(data)
|
||||
var expected_image_types = ['image/gif', 'image/animatedgif']
|
||||
var image_type = data.image.image_type
|
||||
assert.ok(expected_image_types.indexOf(image_type) !== -1, 'got unexpected image type:' + image_type)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('POST media/upload with animated GIF, then POST a tweet referencing the media', function (done) {
|
||||
var b64content = fs.readFileSync(__dirname + '/img/snoopy-animated.gif', { encoding: 'base64' });
|
||||
|
||||
twit.post('media/upload', { media_data: b64content }, function (err, data, response) {
|
||||
assert(!err, err)
|
||||
exports.checkMediaUpload(data)
|
||||
var expected_image_types = ['image/gif', 'image/animatedgif']
|
||||
var image_type = data.image.image_type
|
||||
assert.ok(expected_image_types.indexOf(image_type) !== -1, 'got unexpected image type:' + image_type)
|
||||
|
||||
var mediaIdStr = data.media_id_string
|
||||
assert(mediaIdStr)
|
||||
var params = { status: '#nofilter', media_ids: [mediaIdStr] }
|
||||
twit.post('statuses/update', params, function (err, data, response) {
|
||||
assert(!err, err)
|
||||
var tweetIdStr = data.id_str
|
||||
assert(tweetIdStr)
|
||||
|
||||
exports.req_with_retries(twit, 3, 'post', 'statuses/destroy/:id', { id: tweetIdStr }, [404], function (err, data, response) {
|
||||
checkReply(err, data)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('POST media/upload with animated GIF using `media` parameter', function (done) {
|
||||
var b64Content = fs.readFileSync(__dirname + '/img/snoopy-animated.gif', { encoding: 'base64' });
|
||||
|
||||
twit.post('media/upload', { media: b64Content }, function (err, data, response) {
|
||||
assert(!err, err)
|
||||
exports.checkMediaUpload(data)
|
||||
var expected_image_types = ['image/gif', 'image/animatedgif']
|
||||
var image_type = data.image.image_type
|
||||
assert.ok(expected_image_types.indexOf(image_type) !== -1, 'got unexpected image type:' + image_type)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('POST media/upload with JPG, then POST media/metadata/create with alt text', function (done) {
|
||||
var b64content = fs.readFileSync(__dirname + '/img/bigbird.jpg', { encoding: 'base64' })
|
||||
|
||||
twit.post('media/upload', { media_data: b64content }, function (err, data, response) {
|
||||
assert(!err, err)
|
||||
exports.checkMediaUpload(data)
|
||||
assert.equal(data.image.image_type, 'image/jpeg')
|
||||
|
||||
var mediaIdStr = data.media_id_string
|
||||
assert(mediaIdStr)
|
||||
var altText = 'a very small Big Bird'
|
||||
var params = { media_id: mediaIdStr, alt_text: { text: altText } }
|
||||
twit.post('media/metadata/create', params, function (err, data, response) {
|
||||
assert(!err, err)
|
||||
// data is empty on media/metadata/create success; nothing more to assert
|
||||
done();
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('POST account/update_profile_image', function (done) {
|
||||
var b64content = fs.readFileSync(__dirname + '/img/snoopy-animated.gif', { encoding: 'base64' })
|
||||
|
||||
twit.post('account/update_profile_image', { image: b64content }, function (err, data, response) {
|
||||
assert(!err, err);
|
||||
exports.checkReply(err, data);
|
||||
exports.checkUser(data);
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('POST friendships/create', function (done) {
|
||||
var params = { screen_name: 'tolga_tezel', follow: false };
|
||||
twit.post('friendships/create', params, function (err, data, resp) {
|
||||
assert(!err, err);
|
||||
exports.checkReply(err, data);
|
||||
exports.checkUser(data);
|
||||
done();
|
||||
});
|
||||
})
|
||||
|
||||
describe('Favorites', function () {
|
||||
it('POST favorites/create and POST favorites/destroy work', function (done) {
|
||||
twit.post('favorites/create', { id: '583531943624597504' }, function (err, data, resp) {
|
||||
assert(!err, err);
|
||||
exports.checkReply(err, data);
|
||||
var tweetIdStr = data.id_str;
|
||||
assert(tweetIdStr);
|
||||
|
||||
twit.post('favorites/destroy', { id: tweetIdStr }, function (err, data, resp) {
|
||||
assert(!err, err);
|
||||
exports.checkReply(err, data);
|
||||
assert(data.id_str);
|
||||
assert(data.text);
|
||||
|
||||
done();
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('error handling', function () {
|
||||
describe('handling errors from the twitter api', function () {
|
||||
it('should callback with an Error object with all the info and a response object', function (done) {
|
||||
var twit = new Twit({
|
||||
consumer_key: 'a',
|
||||
consumer_secret: 'b',
|
||||
access_token: 'c',
|
||||
access_token_secret: 'd'
|
||||
})
|
||||
twit.get('account/verify_credentials', function (err, reply, res) {
|
||||
assert(err instanceof Error)
|
||||
assert(err.statusCode === 401)
|
||||
assert(err.code > 0)
|
||||
assert(err.message.match(/token/))
|
||||
assert(err.twitterReply)
|
||||
assert(err.allErrors)
|
||||
assert(res)
|
||||
assert(res.headers)
|
||||
assert.equal(res.statusCode, 401)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
describe('handling other errors', function () {
|
||||
it('should just forward errors raised by underlying request lib', function (done) {
|
||||
var twit = new Twit(config1);
|
||||
var fakeError = new Error('derp')
|
||||
|
||||
var FakeRequest = function () {
|
||||
EventEmitter.call(this)
|
||||
}
|
||||
util.inherits(FakeRequest, EventEmitter)
|
||||
|
||||
var stubGet = function () {
|
||||
var fakeRequest = new FakeRequest()
|
||||
process.nextTick(function () {
|
||||
fakeRequest.emit('error', fakeError)
|
||||
})
|
||||
return fakeRequest
|
||||
}
|
||||
|
||||
var request = require('request')
|
||||
var stubGet = sinon.stub(request, 'get', stubGet)
|
||||
|
||||
twit.get('account/verify_credentials', function (err, reply, res) {
|
||||
assert(err === fakeError)
|
||||
|
||||
// restore request.get
|
||||
stubGet.restore()
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Request timeout', function () {
|
||||
it('set to 1ms should return with a timeout error', function (done) {
|
||||
config1.timeout_ms = 1;
|
||||
var twit = new Twit(config1);
|
||||
twit.get('account/verify_credentials', function (err, reply, res) {
|
||||
assert(err)
|
||||
assert.equal(err.message, 'ETIMEDOUT')
|
||||
delete config1.timeout_ms
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
describe('Twit agent_options config', function () {
|
||||
it('config.trusted_cert_fingerprints works against cert fingerprint for api.twitter.com:443', function (done) {
|
||||
config1.trusted_cert_fingerprints = [
|
||||
'66:EA:47:62:D9:B1:4F:1A:AE:89:5F:68:BA:6B:8E:BB:F8:1D:BF:8E'
|
||||
];
|
||||
var t = new Twit(config1);
|
||||
|
||||
t.get('account/verify_credentials', function (err, data, resp) {
|
||||
assert(!err, err)
|
||||
assert(data)
|
||||
assert(data.id_str)
|
||||
assert(data.name)
|
||||
assert(data.screen_name)
|
||||
|
||||
delete config1.trusted_cert_fingerprints
|
||||
done();
|
||||
})
|
||||
})
|
||||
|
||||
it('config.trusted_cert_fingerprints responds with Error when fingerprint mismatch occurs', function (done) {
|
||||
config1.trusted_cert_fingerprints = [
|
||||
'AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA:AA'
|
||||
];
|
||||
var t = new Twit(config1);
|
||||
|
||||
t.get('account/verify_credentials', function (err, data, resp) {
|
||||
assert(err)
|
||||
assert(err.toString().indexOf('Trusted fingerprints are: ' + config1.trusted_cert_fingerprints[0]) !== -1)
|
||||
|
||||
delete config1.trusted_cert_fingerprints
|
||||
done();
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Local time offset compensation', function () {
|
||||
it('Compensates for local time being behind', function (done) {
|
||||
var t1 = Date.now();
|
||||
var t = new Twit(config2);
|
||||
|
||||
var stubNow = function () {
|
||||
return 0;
|
||||
}
|
||||
var stubDateNow = sinon.stub(Date, 'now', stubNow);
|
||||
|
||||
t.get('account/verify_credentials', function (err, data, resp) {
|
||||
assert(err);
|
||||
|
||||
t.get('account/verify_credentials', function (err, data, resp) {
|
||||
assert(!err, err);
|
||||
exports.checkReply(err, data);
|
||||
exports.checkUser(data);
|
||||
assert(t._twitter_time_minus_local_time_ms > 0)
|
||||
|
||||
stubDateNow.restore();
|
||||
|
||||
done();
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Basic validation to verify we have no error and reply is an object
|
||||
*
|
||||
* @param {error} err error object (or null)
|
||||
* @param {object} reply reply object received from twitter
|
||||
*/
|
||||
var checkReply = exports.checkReply = function (err, reply) {
|
||||
assert.equal(err, null, 'reply err:'+util.inspect(err, true, 10, true))
|
||||
assert.equal(typeof reply, 'object')
|
||||
}
|
||||
|
||||
/**
|
||||
* check the http response object and its headers
|
||||
* @param {object} response http response object
|
||||
*/
|
||||
var checkResponse = exports.checkResponse = function (response) {
|
||||
assert(response)
|
||||
assert(response.headers)
|
||||
assert.equal(response.statusCode, 200)
|
||||
}
|
||||
|
||||
/**
|
||||
* validate that @tweet is a tweet object
|
||||
*
|
||||
* @param {object} tweet `tweet` object received from twitter
|
||||
*/
|
||||
var checkTweet = exports.checkTweet = function (tweet) {
|
||||
assert.ok(tweet)
|
||||
assert.equal('string', typeof tweet.id_str, 'id_str wasnt string:'+tweet.id_str)
|
||||
assert.equal('string', typeof tweet.text)
|
||||
|
||||
assert.ok(tweet.user)
|
||||
assert.equal('string', typeof tweet.user.id_str)
|
||||
assert.equal('string', typeof tweet.user.screen_name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that @dm is a direct message object
|
||||
*
|
||||
* @param {object} dm `direct message` object received from twitter
|
||||
*/
|
||||
exports.checkDm = function checkDm (dm) {
|
||||
assert.ok(dm)
|
||||
assert.equal('string', typeof dm.id_str)
|
||||
assert.equal('string', typeof dm.text)
|
||||
|
||||
var recipient = dm.recipient
|
||||
|
||||
assert.ok(recipient)
|
||||
assert.equal('string', typeof recipient.id_str)
|
||||
assert.equal('string', typeof recipient.screen_name)
|
||||
|
||||
var sender = dm.sender
|
||||
|
||||
assert.ok(sender)
|
||||
assert.equal('string', typeof sender.id_str)
|
||||
assert.equal('string', typeof sender.screen_name)
|
||||
|
||||
assert.equal('string', typeof dm.text)
|
||||
}
|
||||
|
||||
exports.checkMediaUpload = function checkMediaUpload (data) {
|
||||
assert.ok(data)
|
||||
assert.ok(data.image)
|
||||
assert.ok(data.image.w)
|
||||
assert.ok(data.image.h)
|
||||
assert.ok(data.media_id)
|
||||
assert.equal('string', typeof data.media_id_string)
|
||||
assert.ok(data.size)
|
||||
}
|
||||
|
||||
exports.checkUser = function checkUser (data) {
|
||||
assert.ok(data)
|
||||
assert.ok(data.id_str)
|
||||
assert.ok(data.name)
|
||||
assert.ok(data.screen_name)
|
||||
}
|
||||
|
||||
exports.assertTweetHasText = function (tweet, text) {
|
||||
assert(tweet.text.toLowerCase().indexOf(text) !== -1, 'expected to find '+text+' in text: '+tweet.text);
|
||||
}
|
||||
|
||||
exports.req_with_retries = function (twit_instance, num_tries, verb, path, params, status_codes_to_retry, cb) {
|
||||
twit_instance[verb](path, params, function (err, data, response) {
|
||||
if (!num_tries || (status_codes_to_retry.indexOf(response.statusCode) === -1)) {
|
||||
return cb(err, data, response)
|
||||
}
|
||||
|
||||
exports.req_with_retries(twit_instance, num_tries - 1, verb, path, params, status_codes_to_retry, cb)
|
||||
})
|
||||
}
|
||||
55
node_modules/twit/tests/rest_app_only_auth.js
generated
vendored
Normal file
55
node_modules/twit/tests/rest_app_only_auth.js
generated
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
var assert = require('assert')
|
||||
|
||||
var config1 = require('../config1');
|
||||
var Twit = require('../lib/twitter');
|
||||
var checkReply = require('./rest').checkReply;
|
||||
var checkResponse = require('./rest').checkResponse;
|
||||
var checkTweet = require('./rest').checkTweet;
|
||||
|
||||
describe('REST API using app-only auth', function () {
|
||||
var twit = null
|
||||
before(function () {
|
||||
var config = {
|
||||
consumer_key: config1.consumer_key,
|
||||
consumer_secret: config1.consumer_secret,
|
||||
app_only_auth: true,
|
||||
}
|
||||
twit = new Twit(config)
|
||||
})
|
||||
|
||||
it('GET `application/rate_limit_status`', function (done) {
|
||||
twit.get('application/rate_limit_status', function (err, body, response) {
|
||||
checkReply(err, body)
|
||||
checkResponse(response)
|
||||
assert(body.rate_limit_context)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('GET `application/rate_limit_status with specific resource`', function (done) {
|
||||
var params = { resources: [ 'users', 'search' ]}
|
||||
twit.get('application/rate_limit_status', params, function (err, body, response) {
|
||||
checkReply(err, body)
|
||||
checkResponse(response)
|
||||
assert(body.rate_limit_context)
|
||||
assert(body.resources.users)
|
||||
assert(body.resources.search)
|
||||
assert.equal(Object.keys(body.resources).length, 2)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('GET `search/tweets` { q: "a", since_id: 12345 }', function (done) {
|
||||
var params = { q: 'a', since_id: 12345 }
|
||||
twit.get('search/tweets', params, function (err, reply, response) {
|
||||
checkReply(err, reply)
|
||||
assert.ok(reply.statuses)
|
||||
checkTweet(reply.statuses[0])
|
||||
|
||||
checkResponse(response)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
90
node_modules/twit/tests/rest_chunked_upload.js
generated
vendored
Normal file
90
node_modules/twit/tests/rest_chunked_upload.js
generated
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
var assert = require('assert');
|
||||
var fs = require('fs');
|
||||
var mime = require('mime');
|
||||
var path = require('path');
|
||||
|
||||
var config = require('../config1');
|
||||
var Twit = require('../lib/twitter');
|
||||
|
||||
describe('twit.postMediaChunked', function () {
|
||||
it('Posting media via twit.postMediaChunked works with .mp4', function (done) {
|
||||
var twit = new Twit(config);
|
||||
var mediaFilePath = path.join(__dirname, './video/station.mp4');
|
||||
twit.postMediaChunked({ file_path: mediaFilePath }, function (err, bodyObj, resp) {
|
||||
exports.checkUploadMedia(err, bodyObj, resp)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('POST media/upload via manual commands works with .mp4', function (done) {
|
||||
var mediaFilePath = path.join(__dirname, './video/station.mp4');
|
||||
var mediaType = mime.lookup(mediaFilePath);
|
||||
var mediaFileSizeBytes = fs.statSync(mediaFilePath).size;
|
||||
|
||||
var twit = new Twit(config);
|
||||
twit.post('media/upload', {
|
||||
'command': 'INIT',
|
||||
'media_type': mediaType,
|
||||
'total_bytes': mediaFileSizeBytes
|
||||
}, function (err, bodyObj, resp) {
|
||||
assert(!err, err);
|
||||
var mediaIdStr = bodyObj.media_id_string;
|
||||
|
||||
var isStreamingFile = true;
|
||||
var isUploading = false;
|
||||
var segmentIndex = 0;
|
||||
var fStream = fs.createReadStream(mediaFilePath, { highWaterMark: 5 * 1024 * 1024 });
|
||||
|
||||
var _finalizeMedia = function (mediaIdStr, cb) {
|
||||
twit.post('media/upload', {
|
||||
'command': 'FINALIZE',
|
||||
'media_id': mediaIdStr
|
||||
}, cb)
|
||||
}
|
||||
|
||||
var _checkFinalizeResp = function (err, bodyObj, resp) {
|
||||
exports.checkUploadMedia(err, bodyObj, resp)
|
||||
done();
|
||||
}
|
||||
|
||||
fStream.on('data', function (buff) {
|
||||
fStream.pause();
|
||||
isStreamingFile = false;
|
||||
isUploading = true;
|
||||
|
||||
twit.post('media/upload', {
|
||||
'command': 'APPEND',
|
||||
'media_id': mediaIdStr,
|
||||
'segment_index': segmentIndex,
|
||||
'media': buff.toString('base64'),
|
||||
}, function (err, bodyObj, resp) {
|
||||
assert(!err, err);
|
||||
isUploading = false;
|
||||
|
||||
if (!isStreamingFile) {
|
||||
_finalizeMedia(mediaIdStr, _checkFinalizeResp);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
fStream.on('end', function () {
|
||||
isStreamingFile = false;
|
||||
|
||||
if (!isUploading) {
|
||||
_finalizeMedia(mediaIdStr, _checkFinalizeResp);
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
exports.checkUploadMedia = function (err, bodyObj, resp) {
|
||||
assert(!err, err)
|
||||
|
||||
assert(bodyObj)
|
||||
assert(bodyObj.media_id)
|
||||
assert(bodyObj.media_id_string)
|
||||
assert(bodyObj.size)
|
||||
assert(bodyObj.video)
|
||||
assert.equal(bodyObj.video.video_type, 'video/mp4')
|
||||
}
|
||||
646
node_modules/twit/tests/streaming.js
generated
vendored
Normal file
646
node_modules/twit/tests/streaming.js
generated
vendored
Normal file
@@ -0,0 +1,646 @@
|
||||
var assert = require('assert')
|
||||
, http = require('http')
|
||||
, EventEmitter = require('events').EventEmitter
|
||||
, rewire = require('rewire')
|
||||
, sinon = require('sinon')
|
||||
, Twit = require('../lib/twitter')
|
||||
, config1 = require('../config1')
|
||||
, config2 = require('../config2')
|
||||
, colors = require('colors')
|
||||
, helpers = require('./helpers')
|
||||
, util = require('util')
|
||||
, zlib = require('zlib')
|
||||
, async = require('async')
|
||||
, restTest = require('./rest');
|
||||
|
||||
/**
|
||||
* Stop the stream and check the tweet we got back.
|
||||
* Call @done on completion.
|
||||
*
|
||||
* @param {object} stream object returned by twit.stream()
|
||||
* @param {Function} done completion callback
|
||||
*/
|
||||
exports.checkStream = function (stream, done) {
|
||||
stream.on('connected', function () {
|
||||
console.log('\nconnected'.grey)
|
||||
});
|
||||
|
||||
stream.once('tweet', function (tweet) {
|
||||
stream.stop()
|
||||
assert.ok(tweet)
|
||||
assert.equal('string', typeof tweet.text)
|
||||
assert.equal('string', typeof tweet.id_str)
|
||||
|
||||
console.log(('\ntweet: '+tweet.text).grey)
|
||||
|
||||
done()
|
||||
});
|
||||
|
||||
stream.on('reconnecting', function (req, res, connectInterval) {
|
||||
console.log('Got disconnected. Scheduling reconnect! statusCode:', res.statusCode, 'connectInterval', connectInterval)
|
||||
});
|
||||
|
||||
stream.on('error', function (err) {
|
||||
console.log('Stream emitted an error', err)
|
||||
return done(err)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the stream state is correctly set for a stopped stream.
|
||||
*
|
||||
* @param {object} stream object returned by twit.stream()
|
||||
*/
|
||||
exports.checkStreamStopState = function (stream) {
|
||||
assert.strictEqual(stream._connectInterval, 0)
|
||||
assert.strictEqual(stream._usedFirstReconnect, false)
|
||||
assert.strictEqual(stream._scheduledReconnect, undefined)
|
||||
assert.strictEqual(stream._stallAbortTimeout, undefined)
|
||||
}
|
||||
|
||||
describe('Streaming API', function () {
|
||||
|
||||
it('statuses/sample', function (done) {
|
||||
var twit = new Twit(config1);
|
||||
var stream = twit.stream('statuses/sample')
|
||||
|
||||
exports.checkStream(stream, done)
|
||||
})
|
||||
|
||||
it('statuses/filter using `track`', function (done) {
|
||||
this.timeout(120000)
|
||||
var twit = new Twit(config2);
|
||||
var stream = twit.stream('statuses/filter', { track: 'fun' })
|
||||
|
||||
exports.checkStream(stream, done)
|
||||
})
|
||||
|
||||
it('statuses/filter using `locations` string', function (done) {
|
||||
var twit = new Twit(config1);
|
||||
var world = '-180,-90,180,90';
|
||||
var stream = twit.stream('statuses/filter', { locations: world })
|
||||
|
||||
exports.checkStream(stream, done)
|
||||
})
|
||||
|
||||
it('statuses/filter using `locations` array for San Francisco and New York', function (done) {
|
||||
var twit = new Twit(config2);
|
||||
var params = {
|
||||
locations: [ '-122.75', '36.8', '121.75', '37.8', '-74', '40', '73', '41' ]
|
||||
}
|
||||
|
||||
var stream = twit.stream('statuses/filter', params)
|
||||
|
||||
exports.checkStream(stream, done)
|
||||
})
|
||||
|
||||
it('statuses/filter using `track` array', function (done) {
|
||||
var twit = new Twit(config1);
|
||||
var params = {
|
||||
track: [ 'twitter', ':)', 'fun' ]
|
||||
}
|
||||
|
||||
var stream = twit.stream('statuses/filter', params)
|
||||
|
||||
exports.checkStream(stream, done)
|
||||
})
|
||||
|
||||
it('statuses/filter using `track` and `language`', function (done) {
|
||||
var twit = new Twit(config1);
|
||||
var params = {
|
||||
track: [ 'twitter', '#apple', 'google', 'twitter', 'facebook', 'happy', 'party', ':)' ],
|
||||
language: 'en'
|
||||
}
|
||||
|
||||
var stream = twit.stream('statuses/filter', params)
|
||||
|
||||
exports.checkStream(stream, done)
|
||||
})
|
||||
|
||||
it('stopping & restarting the stream works', function (done) {
|
||||
var twit = new Twit(config2);
|
||||
var stream = twit.stream('statuses/sample')
|
||||
|
||||
//stop the stream after 2 seconds
|
||||
setTimeout(function () {
|
||||
stream.stop()
|
||||
|
||||
exports.checkStreamStopState(stream)
|
||||
|
||||
console.log('\nstopped stream')
|
||||
}, 2000)
|
||||
|
||||
//after 3 seconds, start the stream, and stop after 'connect'
|
||||
setTimeout(function () {
|
||||
stream.once('connected', function (req) {
|
||||
console.log('\nrestarted stream')
|
||||
stream.stop()
|
||||
|
||||
exports.checkStreamStopState(stream)
|
||||
|
||||
console.log('\nstopped stream')
|
||||
done()
|
||||
})
|
||||
|
||||
//restart the stream
|
||||
stream.start()
|
||||
}, 3000)
|
||||
})
|
||||
|
||||
it('stopping & restarting stream emits to previously assigned callbacks', function (done) {
|
||||
var twit = new Twit(config1);
|
||||
var stream = twit.stream('statuses/sample')
|
||||
|
||||
var started = false
|
||||
var numTweets = 0
|
||||
stream.on('tweet', function (tweet) {
|
||||
process.stdout.write('.')
|
||||
if (!started) {
|
||||
started = true
|
||||
numTweets++
|
||||
console.log('received tweet', numTweets)
|
||||
|
||||
console.log('stopping stream')
|
||||
stream.stop()
|
||||
|
||||
exports.checkStreamStopState(stream)
|
||||
|
||||
// we've successfully received a new tweet after restarting, test successful
|
||||
if (numTweets === 2) {
|
||||
done()
|
||||
} else {
|
||||
started = false
|
||||
console.log('restarting stream')
|
||||
|
||||
setTimeout(function () {
|
||||
stream.start()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
stream.on('limit', function (limitMsg) {
|
||||
console.log('limit', limitMsg)
|
||||
})
|
||||
|
||||
stream.on('disconnect', function (disconnMsg) {
|
||||
console.log('disconnect', disconnMsg)
|
||||
})
|
||||
|
||||
stream.on('reconnect', function (req, res, ival) {
|
||||
console.log('reconnect. statusCode:', res.statusCode, 'interval:', ival)
|
||||
})
|
||||
|
||||
stream.on('connect', function (req) {
|
||||
console.log('connect')
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
describe('streaming API direct message events', function () {
|
||||
var senderScreenName;
|
||||
var receiverScreenName;
|
||||
var twitSender;
|
||||
var twitReceiver;
|
||||
|
||||
// before we send direct messages the user receiving the DM
|
||||
// has to follow the sender. Make this so.
|
||||
before(function (done) {
|
||||
twitSender = new Twit(config1);
|
||||
twitReceiver = new Twit(config2);
|
||||
|
||||
// get sender/receiver names in parallel, then make the receiver follow the sender
|
||||
async.parallel({
|
||||
// get sender screen name and set it for tests to use
|
||||
getSenderScreenName: function (parNext) {
|
||||
console.log('getting sender user screen_name')
|
||||
|
||||
twitSender.get('account/verify_credentials', { twit_options: { retry: true } }, function (err, reply) {
|
||||
assert(!err, err)
|
||||
|
||||
assert(reply)
|
||||
assert(reply.screen_name)
|
||||
|
||||
senderScreenName = reply.screen_name
|
||||
|
||||
return parNext()
|
||||
})
|
||||
},
|
||||
// get receiver screen name and set it for tests to use
|
||||
getReceiverScreenName: function (parNext) {
|
||||
console.log('getting receiver user screen_name')
|
||||
twitReceiver.get('account/verify_credentials', { twit_options: { retry: true } }, function (err, reply) {
|
||||
assert(!err, err)
|
||||
|
||||
assert(reply)
|
||||
assert(reply.screen_name)
|
||||
|
||||
receiverScreenName = reply.screen_name
|
||||
|
||||
return parNext()
|
||||
})
|
||||
}
|
||||
}, function (err) {
|
||||
assert(!err, err)
|
||||
|
||||
var followParams = { screen_name: senderScreenName }
|
||||
console.log('making receiver user follow the sender user')
|
||||
// make receiver follow sender
|
||||
twitReceiver.post('friendships/create', followParams, function (err, reply) {
|
||||
assert(!err, err)
|
||||
assert(reply.following)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('user_stream `direct_message` event', function (done) {
|
||||
// User A follows User B
|
||||
// User A connects to their user stream
|
||||
// User B posts a DM to User A
|
||||
// User A receives it in their user stream
|
||||
this.timeout(0);
|
||||
|
||||
// build out DM params
|
||||
function makeDmParams () {
|
||||
return {
|
||||
screen_name: receiverScreenName,
|
||||
text: helpers.generateRandomString(10) + ' direct message streaming event test! :-) ' + helpers.generateRandomString(20),
|
||||
twit_options: {
|
||||
retry: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var dmIdsReceived = []
|
||||
var dmIdsSent = []
|
||||
var sentDmFound = false
|
||||
|
||||
// start listening for user stream events
|
||||
var receiverStream = twitReceiver.stream('user')
|
||||
|
||||
console.log('\nlistening for DMs')
|
||||
// listen for direct_message event and check DM once it's received
|
||||
receiverStream.on('direct_message', function (directMsg) {
|
||||
if (sentDmFound) {
|
||||
// don't call `done` more than once
|
||||
return
|
||||
}
|
||||
|
||||
console.log('got DM event. id:', directMsg.direct_message.id_str)
|
||||
restTest.checkDm(directMsg.direct_message)
|
||||
dmIdsReceived.push(directMsg.direct_message.id_str)
|
||||
|
||||
// make sure one of the DMs sent was found
|
||||
// (we can send multiple DMs if our stream has to reconnect)
|
||||
sentDmFound = dmIdsSent.some(function (dmId) {
|
||||
return dmId == directMsg.direct_message.id_str
|
||||
})
|
||||
|
||||
if (!sentDmFound) {
|
||||
console.log('this DM doesnt match our test DMs - still waiting for a matching one.')
|
||||
console.log('dmIdsSent', dmIdsSent)
|
||||
return
|
||||
}
|
||||
|
||||
receiverStream.stop()
|
||||
return done()
|
||||
})
|
||||
|
||||
var lastTimeSent = 0
|
||||
var msToWait = 0
|
||||
var postDmInterval = null
|
||||
|
||||
receiverStream.on('connected', function () {
|
||||
var dmParams = makeDmParams()
|
||||
|
||||
console.log('sending a new DM:', dmParams.text)
|
||||
twitSender.post('direct_messages/new', dmParams, function (err, reply) {
|
||||
assert(!err, err)
|
||||
assert(reply)
|
||||
restTest.checkDm(reply)
|
||||
assert(reply.id_str)
|
||||
// we will check this dm against the reply recieved in the message event
|
||||
dmIdsSent.push(reply.id_str)
|
||||
|
||||
console.log('successfully posted DM:', reply.text, reply.id_str)
|
||||
if (dmIdsReceived.indexOf(reply.id_str) !== -1) {
|
||||
// our response to the DM posting lost the race against the direct_message
|
||||
// listener (we already got the event). So we can finish the test.
|
||||
done()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
after(function (done) {
|
||||
console.log('cleaning up DMs:', dmIdsSent)
|
||||
// delete the DMs we posted
|
||||
var deleteDms = dmIdsSent.map(function (dmId) {
|
||||
return function (next) {
|
||||
assert.equal(typeof dmId, 'string')
|
||||
console.log('\ndeleting DM', dmId)
|
||||
var params = { id: dmId, twit_options: { retry: true } }
|
||||
twitSender.post('direct_messages/destroy', params, function (err, reply) {
|
||||
assert(!err, err)
|
||||
restTest.checkDm(reply)
|
||||
assert.equal(reply.id, dmId)
|
||||
return next()
|
||||
})
|
||||
}
|
||||
})
|
||||
async.parallel(deleteDms, done)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('streaming API friends preamble', function () {
|
||||
it('returns an array of strings if stringify_friend_ids is true', function (done) {
|
||||
var twit = new Twit(config1);
|
||||
var stream = twit.stream('user', { stringify_friend_ids: true });
|
||||
stream.on('friends', function (friendsObj) {
|
||||
assert(friendsObj)
|
||||
assert(friendsObj.friends_str)
|
||||
if (friendsObj.friends_str.length) {
|
||||
assert.equal(typeof friendsObj.friends_str[0], 'string')
|
||||
} else {
|
||||
console.log('\nEmpty friends preamble:', friendsObj, '. Make some friends on Twitter! ^_^')
|
||||
}
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('streaming API bad request', function (done) {
|
||||
it('emits an error for a 401 response', function (done) {
|
||||
var badCredentials = {
|
||||
consumer_key: 'a'
|
||||
, consumer_secret: 'b'
|
||||
, access_token: 'c'
|
||||
, access_token_secret: 'd'
|
||||
}
|
||||
|
||||
var twit = new Twit(badCredentials);
|
||||
|
||||
var stream = twit.stream('statuses/filter', { track : ['foo'] });
|
||||
|
||||
stream.on('parser-error', function (err) {
|
||||
assert.equal(err.statusCode, 401)
|
||||
assert(err.twitterReply)
|
||||
|
||||
return done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('streaming API `messages` event', function (done) {
|
||||
var request = require('request');
|
||||
var originalPost = request.post;
|
||||
var RewiredTwit = rewire('../lib/twitter');
|
||||
var RewiredStreamingApiConnection = rewire('../lib/streaming-api-connection');
|
||||
var revertParser, revertTwit;
|
||||
|
||||
var MockParser = function () {
|
||||
var self = this;
|
||||
EventEmitter.call(self);
|
||||
process.nextTick(function () {
|
||||
self.emit('element', {scrub_geo: 'bar'})
|
||||
self.emit('element', {limit: 'buzz'})
|
||||
});
|
||||
}
|
||||
util.inherits(MockParser, EventEmitter);
|
||||
|
||||
before(function () {
|
||||
revertTwit = RewiredTwit.__set__('StreamingAPIConnection', RewiredStreamingApiConnection);
|
||||
revertParser = RewiredStreamingApiConnection.__set__('Parser', MockParser);
|
||||
|
||||
request.post = function () { return new helpers.FakeRequest() }
|
||||
})
|
||||
|
||||
after(function () {
|
||||
request.post = originalPost;
|
||||
revertTwit();
|
||||
revertParser();
|
||||
})
|
||||
|
||||
it('is returned for 2 different event types', function (done) {
|
||||
var twit = new RewiredTwit(config1);
|
||||
var stream = twit.stream('statuses/sample');
|
||||
var gotScrubGeo = false;
|
||||
var gotLimit = false;
|
||||
var numMessages = 0;
|
||||
|
||||
var maybeDone = function () {
|
||||
if (gotScrubGeo && gotLimit && numMessages == 2) {
|
||||
done()
|
||||
}
|
||||
}
|
||||
|
||||
stream.on('limit', function () {
|
||||
gotLimit = true;
|
||||
maybeDone();
|
||||
});
|
||||
stream.on('scrub_geo', function () {
|
||||
gotScrubGeo = true;
|
||||
maybeDone();
|
||||
})
|
||||
|
||||
stream.on('message', function (msg) {
|
||||
numMessages++;
|
||||
maybeDone();
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('streaming reconnect', function (done) {
|
||||
it('correctly implements connection closing backoff', function (done) {
|
||||
var stubPost = function () {
|
||||
var fakeRequest = new helpers.FakeRequest()
|
||||
process.nextTick(function () {
|
||||
fakeRequest.emit('close')
|
||||
})
|
||||
return fakeRequest
|
||||
}
|
||||
|
||||
var request = require('request')
|
||||
var stubPost = sinon.stub(request, 'post', stubPost)
|
||||
|
||||
var twit = new Twit(config1);
|
||||
var stream = twit.stream('statuses/filter', { track: [ 'fun', 'yolo']});
|
||||
|
||||
var reconnects = [0, 250, 500, 750]
|
||||
var reconnectCount = -1
|
||||
|
||||
var testDone = false
|
||||
|
||||
stream.on('reconnect', function () {
|
||||
if (testDone) {
|
||||
return
|
||||
}
|
||||
reconnectCount += 1
|
||||
var expectedInterval = reconnects[reconnectCount]
|
||||
|
||||
// make sure our connect interval is correct
|
||||
assert.equal(stream._connectInterval, expectedInterval);
|
||||
|
||||
// simulate immediate reconnect by forcing a new connection (`self._connectInterval` parameter unchanged)
|
||||
stream._startPersistentConnection();
|
||||
|
||||
if (reconnectCount === reconnects.length -1) {
|
||||
// restore request.post
|
||||
stubPost.restore()
|
||||
testDone = true
|
||||
return done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('correctly implements 420 backoff', function (done) {
|
||||
var stubPost = function () {
|
||||
var fakeRequest = new helpers.FakeRequest()
|
||||
process.nextTick(function () {
|
||||
var fakeResponse = new helpers.FakeResponse(420)
|
||||
fakeRequest.emit('response', fakeResponse)
|
||||
fakeRequest.emit('close')
|
||||
})
|
||||
return fakeRequest
|
||||
}
|
||||
|
||||
var request = require('request')
|
||||
var stubPost = sinon.stub(request, 'post', stubPost)
|
||||
|
||||
var twit = new Twit(config1);
|
||||
var stream = twit.stream('statuses/filter', { track: [ 'fun', 'yolo']});
|
||||
|
||||
var reconnects = [60000, 120000, 240000, 480000]
|
||||
var reconnectCount = -1
|
||||
var testComplete = false
|
||||
|
||||
stream.on('reconnect', function (req, res, connectInterval) {
|
||||
if (testComplete) {
|
||||
// prevent race between last connection attempt firing a reconnect and us validating the final
|
||||
// reconnect value in `reconnects`
|
||||
return
|
||||
}
|
||||
|
||||
reconnectCount += 1
|
||||
var expectedInterval = reconnects[reconnectCount]
|
||||
|
||||
// make sure our connect interval is correct
|
||||
assert.equal(stream._connectInterval, connectInterval);
|
||||
assert.equal(stream._connectInterval, expectedInterval);
|
||||
// simulate immediate reconnect by forcing a new connection (`self._connectInterval` parameter unchanged)
|
||||
stream._startPersistentConnection();
|
||||
|
||||
if (reconnectCount === reconnects.length -1) {
|
||||
// restore request.post
|
||||
stubPost.restore()
|
||||
testComplete = true
|
||||
return done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Streaming API disconnect message', function (done) {
|
||||
it('results in stopping the stream', function (done) {
|
||||
var stubPost = function () {
|
||||
var fakeRequest = new helpers.FakeRequest()
|
||||
process.nextTick(function () {
|
||||
var body = zlib.gzipSync(JSON.stringify({disconnect: true}) + '\r\n')
|
||||
var fakeResponse = new helpers.FakeResponse(200, body)
|
||||
fakeRequest.emit('response', fakeResponse);
|
||||
fakeResponse.emit('close')
|
||||
});
|
||||
return fakeRequest
|
||||
}
|
||||
|
||||
var request = require('request')
|
||||
var origRequest = request.post
|
||||
var stubs = sinon.collection
|
||||
stubs.stub(request, 'post', stubPost)
|
||||
|
||||
var twit = new Twit(config1);
|
||||
var stream = twit.stream('statuses/filter', { track: ['fun']});
|
||||
|
||||
stream.on('disconnect', function (disconnMsg) {
|
||||
stream.stop();
|
||||
// restore stub
|
||||
request.post = origRequest
|
||||
done();
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
describe('Streaming API Connection limit exceeded message', function (done) {
|
||||
it('results in an `error` event containing the message', function (done) {
|
||||
var errMsg = 'Exceeded connection limit for user';
|
||||
|
||||
var stubPost = function () {
|
||||
var fakeRequest = new helpers.FakeRequest();
|
||||
process.nextTick(function () {
|
||||
var body = zlib.gzipSync(errMsg + '\r\n');
|
||||
var fakeResponse = new helpers.FakeResponse(200, body);
|
||||
fakeRequest.emit('response', fakeResponse);
|
||||
fakeResponse.emit('close');
|
||||
});
|
||||
return fakeRequest
|
||||
}
|
||||
|
||||
var request = require('request');
|
||||
var origRequest = request.post;
|
||||
var stubs = sinon.collection;
|
||||
stubs.stub(request, 'post', stubPost);
|
||||
|
||||
var twit = new Twit(config1);
|
||||
var stream = twit.stream('statuses/filter');
|
||||
|
||||
stream.on('error', function (err) {
|
||||
assert(err.toString().indexOf(errMsg) !== -1, 'Unexpected error msg:' + errMsg + '.');;
|
||||
stream.stop();
|
||||
// restore stub
|
||||
request.post = origRequest;
|
||||
done();
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Streaming API connection management', function () {
|
||||
it('.stop() works in all states', function (done) {
|
||||
var stubPost = function () {
|
||||
var fakeRequest = new helpers.FakeRequest();
|
||||
process.nextTick(function () {
|
||||
var body = zlib.gzipSync('Foobar\r\n');
|
||||
var fakeResponse = new helpers.FakeResponse(200, body);
|
||||
fakeRequest.emit('response', fakeResponse);
|
||||
});
|
||||
return fakeRequest
|
||||
}
|
||||
|
||||
var request = require('request');
|
||||
var origRequest = request.post;
|
||||
var stubs = sinon.collection;
|
||||
stubs.stub(request, 'post', stubPost);
|
||||
|
||||
var twit = new Twit(config1);
|
||||
|
||||
var stream = twit.stream('statuses/sample');
|
||||
stream.stop();
|
||||
console.log('\nStopped. Restarting..');
|
||||
stream.start();
|
||||
stream.once('connect', function(request) {
|
||||
console.log('Stream emitted `connect`. Stopping & starting stream..')
|
||||
stream.stop();
|
||||
|
||||
stream.once('connected', function () {
|
||||
console.log('Stream emitted `connected`. Stopping stream.');
|
||||
stream.stop();
|
||||
|
||||
stubs.restore();
|
||||
done();
|
||||
});
|
||||
stream.start();
|
||||
});
|
||||
})
|
||||
})
|
||||
13
node_modules/twit/tests/test_helpers.js
generated
vendored
Normal file
13
node_modules/twit/tests/test_helpers.js
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
var assert = require('assert')
|
||||
var helpers = require('../lib/helpers')
|
||||
|
||||
describe('makeQueryString', function () {
|
||||
it('correctly encodes Objects with String values', function () {
|
||||
assert.equal(helpers.makeQueryString({a: 'Ladies + Gentlemen'}), 'a=Ladies%20%2B%20Gentlemen');
|
||||
assert.equal(helpers.makeQueryString({a: 'An encoded string!'}), 'a=An%20encoded%20string%21');
|
||||
assert.equal(helpers.makeQueryString({a: 'Dogs, Cats & Mice'}), 'a=Dogs%2C%20Cats%20%26%20Mice')
|
||||
assert.equal(helpers.makeQueryString({a: '☃'}), 'a=%E2%98%83')
|
||||
assert.equal(helpers.makeQueryString({a: '#haiku #poetry'}), 'a=%23haiku%20%23poetry')
|
||||
assert.equal(helpers.makeQueryString({a: '"happy hour" :)'}), 'a=%22happy%20hour%22%20%3A%29')
|
||||
})
|
||||
})
|
||||
105
node_modules/twit/tests/twit.js
generated
vendored
Normal file
105
node_modules/twit/tests/twit.js
generated
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
var assert = require('assert')
|
||||
, Twit = require('../lib/twitter')
|
||||
, config1 = require('../config1')
|
||||
|
||||
describe('twit', function () {
|
||||
describe('instantiation', function () {
|
||||
it('works with var twit = new Twit()', function () {
|
||||
var twit = new Twit({
|
||||
consumer_key: 'a',
|
||||
consumer_secret: 'b',
|
||||
access_token: 'c',
|
||||
access_token_secret: 'd'
|
||||
});
|
||||
assert(twit.config)
|
||||
assert.equal(typeof twit.get, 'function')
|
||||
assert.equal(typeof twit.post, 'function')
|
||||
assert.equal(typeof twit.stream, 'function')
|
||||
})
|
||||
it('works with var twit = Twit()', function () {
|
||||
var twit = Twit({
|
||||
consumer_key: 'a',
|
||||
consumer_secret: 'b',
|
||||
access_token: 'c',
|
||||
access_token_secret: 'd'
|
||||
});
|
||||
assert(twit.config)
|
||||
assert.equal(typeof twit.get, 'function')
|
||||
assert.equal(typeof twit.post, 'function')
|
||||
assert.equal(typeof twit.stream, 'function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('config', function () {
|
||||
it('throws when passing empty config', function (done) {
|
||||
assert.throws(function () {
|
||||
var twit = new Twit({})
|
||||
}, Error)
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
it('throws when config is missing a required key', function (done) {
|
||||
assert.throws(function () {
|
||||
var twit = new Twit({
|
||||
consumer_key: 'a'
|
||||
, consumer_secret: 'a'
|
||||
, access_token: 'a'
|
||||
})
|
||||
}, Error)
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
it('throws when config provides all keys but they\'re empty strings', function (done) {
|
||||
assert.throws(function () {
|
||||
var twit = new Twit({
|
||||
consumer_key: ''
|
||||
, consumer_secret: ''
|
||||
, access_token: ''
|
||||
, access_token_secret: ''
|
||||
})
|
||||
}, Error)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
describe('setAuth()', function () {
|
||||
var twit;
|
||||
|
||||
beforeEach(function () {
|
||||
twit = new Twit({
|
||||
consumer_key: 'a',
|
||||
consumer_secret: 'b',
|
||||
access_token: 'c',
|
||||
access_token_secret: 'd'
|
||||
})
|
||||
})
|
||||
|
||||
it('should update the client\'s auth config', function (done) {
|
||||
// partial update
|
||||
twit.setAuth({
|
||||
consumer_key: 'x',
|
||||
consumer_secret: 'y'
|
||||
})
|
||||
|
||||
assert(twit.config.consumer_key === 'x')
|
||||
assert(twit.config.consumer_secret === 'y')
|
||||
|
||||
// full update
|
||||
twit.setAuth(config1)
|
||||
|
||||
assert(twit.config.consumer_key === config1.consumer_key)
|
||||
assert(twit.config.consumer_secret === config1.consumer_secret)
|
||||
assert(twit.config.access_token === config1.access_token)
|
||||
assert(twit.config.access_token_secret === config1.access_token_secret)
|
||||
|
||||
twit.get('account/verify_credentials', { twit_options: { retry: true } }, function (err, reply, response) {
|
||||
assert(!err, err);
|
||||
assert(response.headers['x-rate-limit-limit'])
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
38
node_modules/twit/tests/user_stream.js
generated
vendored
Normal file
38
node_modules/twit/tests/user_stream.js
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
var assert = require('assert')
|
||||
var Twit = require('../lib/twitter')
|
||||
var config1 = require('../config1')
|
||||
var streaming = require('./streaming')
|
||||
|
||||
//verify `friendsMsg` is a twitter 'friends' message object
|
||||
function checkFriendsMsg (friendsMsg) {
|
||||
var friendIds = friendsMsg.friends
|
||||
|
||||
assert(friendIds)
|
||||
assert(Array.isArray(friendIds))
|
||||
assert(friendIds[0])
|
||||
}
|
||||
|
||||
describe('user events', function () {
|
||||
it('friends', function (done) {
|
||||
var twit = new Twit(config1);
|
||||
var stream = twit.stream('user');
|
||||
|
||||
//make sure we're connected to the right endpoint
|
||||
assert.equal(stream.reqOpts.url, 'https://userstream.twitter.com/1.1/user.json')
|
||||
|
||||
stream.on('friends', function (friendsMsg) {
|
||||
checkFriendsMsg(friendsMsg)
|
||||
|
||||
stream.stop()
|
||||
done()
|
||||
})
|
||||
|
||||
stream.on('connect', function () {
|
||||
console.log('\nuser stream connecting..')
|
||||
})
|
||||
|
||||
stream.on('connected', function () {
|
||||
console.log('user stream connected.')
|
||||
})
|
||||
})
|
||||
})
|
||||
BIN
node_modules/twit/tests/video/station.mp4
generated
vendored
Normal file
BIN
node_modules/twit/tests/video/station.mp4
generated
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user