src/main.js
/**
* NTwitBot - main.js
* @file Primary application logic.
* @author Jordan Sne <jordansne@gmail.com>
* @license MIT
*/
const timers = require('timers');
const Data = require('./data.js');
const Generate = require('./generate.js');
const Process = require('./process.js');
const Retrieve = require('./retrieve.js');
const Twitter = require('./twitter.js');
const Utils = require('./utils.js');
/**
* Primary class of the bot. Handles all primary functions.
*/
class Main {
/**
* Initialize bot modules.
* @param {Object} secretData - The secret data for the Twitter API.
* @param {Object} setup - The setup object from the config.
*/
constructor(secretData, setup) {
// 15 minutes
this.TWEET_INTERVAL = 15 * 60 * 1000;
this.secretData = secretData;
this.setup = setup;
Utils.log(`Starting NTwitBot ${process.env.npm_package_version}..`);
Utils.setDebug(this.setup.debug);
this.dataHandler = new Data();
this.processor = new Process();
this.generator = new Generate();
this.twitterHandler = new Twitter(this.secretData);
this.retriever = new Retrieve(this.twitterHandler);
}
/**
* Initialize and setup bot.
* @returns {Promise} Resolves when done setting up.
*/
init() {
return this.twitterHandler.verify().then((userID) => {
Utils.log(`Verified Bot credentials, User ID is: ${userID}`);
return this.initState();
}).then(() => (
this.dataHandler.createDataDir()
)).catch((error) => {
Utils.logError('FATAL: Failed to initialize bot', error);
throw new Error('Initializaion failure');
});
}
/**
* Initializes the state object.
* @private
* @return {Promise} Resolves when done setting the state.
*/
initState() {
return this.dataHandler.readState().then((state) => {
if (state === null) {
this.state = {
trackedUsers: {}, // String dictionary with format 'userID : lastTweetID'
lastMention: 0 // Tweet ID of Last mention
};
} else {
this.state = state;
}
});
}
/**
* Starts the event handler.
* @returns {void}
*/
start() {
this.runUpdate();
timers.setInterval(() => {
this.runUpdate();
}, this.TWEET_INTERVAL);
}
/**
* Scheduled 15 min interval bot update.
* @returns {void}
*/
runUpdate() {
let updateState = false;
Utils.log('');
Utils.log('******************* Running update ******************* ');
Utils.log('');
return this.handleTweets().then((newTweets) => {
if (newTweets) {
updateState = true;
}
return this.handleMentions();
}).then((newMentions) => {
if (newMentions) {
updateState = true;
}
if (updateState) {
return this.dataHandler.saveState(this.state);
}
}, (error) => {
Utils.logError('Failed to handle new mentions (skipping until next update)', error);
}).then(() => {
this.sendTweet();
});
}
/**
* Handles any new tweets from tracked users. Terminates bot upon save failure.
* @return {Promise} Resolves with a boolean if new tweets were retrievd when done processing.
*/
handleTweets() {
return this.updateTracked().then(() => (
this.retriever.retrieveTweets(this.state.trackedUsers)
)).then((retrievals) => {
const tweets = this.processRetrievals(retrievals);
if (tweets.length > 0) {
Utils.log(`Retrieved tweets: ${tweets.length} tweets to process`);
return this.dataHandler.saveTweetData(this.processor.processTweets(tweets));
} else {
Utils.log('Retrieved tweets: No tweets to process');
return Promise.reject();
}
}, (error) => {
Utils.logError('Failed to retrieve new tweets, skipping until next update', error);
return Promise.reject();
}).then(() => true, (error) => {
if (error) {
Utils.logError('FATAL: Failed to save tweet data in database', error);
throw new Error('Database error');
}
return false;
});
}
/**
* Combines tweet retrievals to a single array of tweets and updates the state.
* @private
* @param {Object[]} retrievals - An array of tweet retrievals (array).
* @return {Object[]} The array of all received tweets.
*/
processRetrievals(retrievals) {
const tweets = [];
for (const retrieval of retrievals) {
if (retrieval.length > 0) {
const firstTweet = retrieval[0];
this.state.trackedUsers[firstTweet.user.id_str] = firstTweet.id_str;
for (const tweet of retrieval) {
if (!tweet.hasOwnProperty('retweeted_status')) {
tweets.push(tweet);
}
}
}
}
return tweets;
}
/**
* Updates the currently tracked users with the bot's following list.
* @return {Promise} Resolves when done updating.
*/
updateTracked() {
return this.twitterHandler.getFollowing().then((following) => {
const ids = following.ids;
// Add any new follows to the trackedUsers list
for (const user of ids) {
if (!(user in this.state.trackedUsers)) {
this.state.trackedUsers[user] = 0;
}
}
// Remove any trackedUsers that the bot is no longer following
for (const user in this.state.trackedUsers) {
if (!Utils.isInArray(ids, user)) {
delete this.state.trackedUsers[user];
}
}
});
}
/**
* Handles any new mentions of the bot.
* @return {Promise} Resolves with a boolean if new mentions were found when done retrieving and handling metions.
*/
handleMentions() {
return this.retriever.retrieveMentions(this.state.lastMention).then((mentions) => {
const tweetsToSend = [];
if (mentions.length > 0) {
Utils.log(`Retrieved mentions: ${mentions.length} new tweets found`);
for (const mention of mentions) {
tweetsToSend.push({
replyToID: mention.id_str,
replyToName: mention.user.screen_name
});
}
this.state.lastMention = mentions[0].id_str;
for (const tweet of tweetsToSend) {
this.sendTweet(tweet.replyToID, tweet.replyToName);
}
return true;
} else {
Utils.log('Retrieved mentions: No new tweets found');
return false;
}
}, (error) => {
Utils.logError('Failed to retrieve new mentions (skipping)', error);
});
}
/**
* Generate and sends a tweet. Terminates bot upon database failure.
* @param {string} [replyID] The ID of the user to reply to.
* @param {string} [replyUser] The username of the user to reply to.
* @return {Promise} Resolves when complete (successful or not)
*/
sendTweet(replyID, replyUser) {
return this.dataHandler.readTweetData().then((data) => {
const tweet = this.generator.generateTweet(data);
return this.twitterHandler.postTweet(tweet, replyID, replyUser).then(() => {
Utils.log(`Generated & sent tweet: ${tweet}`);
}, (error) => {
Utils.logError('Failed to send tweet: Posting tweet (skipping)', error);
// TODO Determine if retryable and retry/exit depending on result
});
}, (error) => {
Utils.logError('FATAL: Failed to send tweet: Database error', error);
throw new Error('Database error');
});
}
}
module.exports = Main;