jordansne/ntwitbot

View on GitHub
src/retrieve.js

Summary

Maintainability
A
3 hrs
Test Coverage
/**
 * NTwitBot - retrieve.js
 * @file High level Twitter data retriever.
 * @author Jordan Sne <jordansne@gmail.com>
 * @license MIT
 */

const Utils = require('./utils.js');

/**
 * Data retriever class. Retrieves and pre-processes data from Twitter.
 */
class Retrieve {

    /**
     * Initialize the class.
     * @param {Twitter} twitterHandler - The Twitter API object for making API calls.
     */
    constructor(twitterHandler) {
        this.twitterHandler = twitterHandler;
        this.TWEETS_TO_TRACK = 3000;
    }

    /**
     * Retrieves new mentions of the bot, and saves the current state if needed.
     * @param {string} lastID - The last mention that was received.
     * @return {Promise} Resolves with any new mentions of the bot.
     */
    retrieveMentions(lastID) {
        const mentionData = {};

        // Retrieve since last mention or last 200
        if (lastID !== 0) {
            mentionData.since_id = lastID;
        } else {
            mentionData.count = 200;
        }

        return this.twitterHandler.getMentions(mentionData);
    }

    /**
     * Retrieves and processed new tweets of tracked users.
     * @param {Object} trackedUsers - The tracked users data from the state.
     * @return {Promise} Resolves with the new tweets.
     */
    retrieveTweets(trackedUsers) {
        const tweetRetrievals = [];

        for (const userID in trackedUsers) {
            const lastTweetID = trackedUsers[userID];
            const requestData = {
                user_id: userID,
                trim_user: true
            };

            // If the user has been processed before
            if (lastTweetID !== 0) {
                requestData.count = 200;
                requestData.since_id = lastTweetID;
                tweetRetrievals.push(this.retrieveForExisting(requestData));
            // .. or the user has NOT been processed before
            } else {
                tweetRetrievals.push(this.retrieveForNew(requestData));
            }
        }

        // Wait until all retrievals are done
        return Promise.all(tweetRetrievals);
    }

    /* eslint-disable no-param-reassign */
    /**
     * Retrieves new tweets from an existing user. Only specify one argument.
     * @private
     * @param {Object} params - The data to be used for making twitter requests.
     * @param {Object[]} [tweetsRetrieved] - Parameter used by recursive call. Leave undefined.
     * @return {Object[]} Resolves with an array of tweets.
     */
    retrieveForExisting(params, tweetsRetrieved) {
        // Copy object to allow for proper unit testing
        const request = Object.assign({}, params);
        const firstReq = !request.hasOwnProperty('max_id');

        // Initialize recursive variables
        if (firstReq) {
            tweetsRetrieved = [];
            Utils.log(`Retrieving most recent tweets from user with ID: "${request.user_id}"`);
        }

        return this.twitterHandler.getTweets(request).then((tweets) => {
            // Do not count the last tweet as it might be the first tweet of the next request
            const numRetrieved = tweets.length - 1;

            if (tweets.length === 0) {
                Utils.log('    No new tweets found');

                return {
                    done: true,
                    tweets: tweetsRetrieved,
                    totalRetrieved: 0
                };
            }

            // If the max ID from the request is the oldest tweet in the request (i.e. only one tweet returned)
            // or only one tweet was found on the first request..
            if (request.max_id === tweets[tweets.length - 1].id_str || (firstReq && tweets.length === 1)) {
                tweetsRetrieved.push(tweets[0]);

                Utils.log('    Retrieved 1 tweet');
                Utils.log(`    Finished retrieving ${tweetsRetrieved.length} tweets`);

                return {
                    done: true,
                    tweets: tweetsRetrieved
                };
            }

            for (let i = 0; i < numRetrieved; i++) {
                tweetsRetrieved.push(tweets[i]);
            }

            Utils.log(`    Retrieved ${numRetrieved} tweets`);

            // Set the max ID of the next request to the oldest tweet in the previous request
            const nextRequest = Object.assign({}, request);
            nextRequest.max_id = tweets[tweets.length - 1].id_str;

            return {
                done: false,
                request: nextRequest,
                tweetsRetrieved
            };
        }).then((next) => {
            if (!next.done) {
                return this.retrieveForExisting(next.request, next.tweetsRetrieved);
            } else {
                return next.tweets;
            }
        });
    }

    /**
     * Retrieves new tweets from a newly added user. Only specify one argument.
     * @private
     * @param {Object} params - The data to be used for making twitter requests.
     * @param {int} [tweetsLeft] - Used by recursive call. Leave undefined.
     * @param {Object[]} [tweetsRetrieved] - Used by recursive call. Leave undefined.
     * @return {Promise} Resolves with an object (The tweet data and total number of tweets retrieved).
     */
    retrieveForNew(params, tweetsLeft, tweetsRetrieved) {
        // Copy object to allow for proper unit testing
        const request = Object.assign({}, params);

        // If first request..
        if (!tweetsLeft) {
            request.count = 200;
            tweetsLeft = this.TWEETS_TO_TRACK;
            tweetsRetrieved = [];

            Utils.log(`New user with ID: "${request.user_id}" added`);
            Utils.log(`    Retrieving most recent ${this.TWEETS_TO_TRACK} tweets`);
        }

        return this.twitterHandler.getTweets(request).then((tweets) => {
            const numRetrieved = tweets.length - 1;

            // If only one tweet was returned and it was the max ID tweet requested..
            if (request.max_id === tweets[tweets.length - 1].id_str) {
                tweetsRetrieved.push(tweets[0]);

                Utils.log(`    Retrieved ${tweetsRetrieved.length}/${this.TWEETS_TO_TRACK} tweets`);
                Utils.log('    Reached twitter\'s max tweet retrieval limit.');
                Utils.log('    Finished retrieving tweets');

                return {
                    done: true,
                    tweets: tweetsRetrieved
                };
            }

            // If it retrieved more tweets than what was needed..
            if (numRetrieved + 1 >= tweetsLeft) {
                for (let i = 0; i < tweetsLeft; i++) {
                    tweetsRetrieved.push(tweets[i]);
                }

                Utils.log(`    Retrieved ${tweetsRetrieved.length}/${this.TWEETS_TO_TRACK} tweets`);
                Utils.log('    Finished retrieving tweets');
                return {
                    done: true,
                    tweets: tweetsRetrieved
                };
            }

            for (let i = 0; i < numRetrieved; i++) {
                tweetsRetrieved.push(tweets[i]);
            }
            tweetsLeft -= numRetrieved;

            Utils.log(`    Retrieved ${tweetsRetrieved.length}/${this.TWEETS_TO_TRACK} tweets`);

            // Set newest ID for next request to oldest ID of the previous request
            const nextRequest = Object.assign({}, request);
            nextRequest.max_id = tweets[tweets.length - 1].id_str;

            return {
                done: false,
                request: nextRequest,
                tweetsLeft,
                tweetsRetrieved
            };
        }).then((next) => {
            if (!next.done) {
                return this.retrieveForNew(next.request, next.tweetsLeft, next.tweetsRetrieved);
            } else {
                return next.tweets;
            }
        });
    }
    /* eslint-enable no-param-reassign */

}

module.exports = Retrieve;