From d11d84dc339fcb23f1ee301400bb502ebeca3d58 Mon Sep 17 00:00:00 2001 From: Stefan Hayden Date: Fri, 6 May 2016 17:50:12 +0000 Subject: [PATCH] Rework in to a module that can be imported to other projects --- .gitignore | 2 - bot.js | 12 +- components/generator/index.js | 1104 ++++++++++++----------- components/robot/index.js | 239 ++--- components/tweets/index.js | 706 ++++++++------- components/utilities/index.js | 13 +- config/{config.example.js => config.js} | 64 +- config/index.js | 2 +- package.json | 3 +- 9 files changed, 1082 insertions(+), 1063 deletions(-) rename config/{config.example.js => config.js} (76%) diff --git a/.gitignore b/.gitignore index c8bd2ab..1ca9571 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ -config/config.js node_modules/ npm-debug.log -tweets.txt diff --git a/bot.js b/bot.js index 7f527ad..0d3a9ec 100644 --- a/bot.js +++ b/bot.js @@ -1,5 +1,5 @@ /** -* nodeEbot v.0.2.0! +* nodeEbot v.0.2.0! * A twitter_ebooks style bot for Node.js * by Dave Schumaker (@davely) * https://github.com/daveschumaker/nodeEbot @@ -12,7 +12,13 @@ * Import the robot component. * This is where all the business logic is handeled. */ -var robot = require('./components/robot'); +var _ = require('lodash'); +var defaultConfig = require('./config'); +var Robot = require('./components/robot'); // Fire up the robot! -robot.init(); \ No newline at end of file +module.exports = function(userConfig) { + config = _.merge(defaultConfig, userConfig); + + return new Robot(config) +} diff --git a/components/generator/index.js b/components/generator/index.js index 20bbeae..6e68901 100644 --- a/components/generator/index.js +++ b/components/generator/index.js @@ -1,597 +1,599 @@ // TEXT GENERATOR! -var config = require('../../config'); - -module.exports = { - +var Generator = function Generator(config) { + this.config = config; // Keep track of all our word stats - dictionary: {}, // This is a new object I'm using to generate sentences in a more efficient manner than the wordpairs array. - startwords: [], // Words that start a sentence. - stopwords: [], // Stop words. - hashtags: [], // Automatically detect any hashtags that we use. - wordpairs: [], // This is where we'll store our dictionary of word pairs - popularKeywords: [], // Track popular keywords that the bot may be interested in based on our tweet history. - - // Build out initial word pair dictionary for creating - // semi-intelligent sounding sentences pseudo Markov chains. - buildCorpus: function(content) { - var countWords = 0; - - // This for-loop will iterate over every single line of text that's passed in from the "content" argument. - // This will be text that's cleaned up and formatted correctly after the server / bot reads data from - // our tweets.txt file. In the case below, content[currentLine] will represent one line of text. - // Example: content[currentLine] === "Oh, man. I'm really hungry!" - for (var currentLine = 0; currentLine < content.length; currentLine++) { - - // In order to start properly building our corpus of processed text, - // we're going to need to split up each word in our sentence individually into an array. - // Since we're splitting on spaces between words, this will attach punctuation marks and the like. - // This is something we actually want! We can check for "end" words and stuff later. - // Example: ['Oh,', 'man.', 'I\'m', 'really', 'hungry!'] - var words = content[currentLine].split(' '); - - // We want our robot to sound intelligent, we track words that start each sentence (new line). - // There are some cases where this currently falls apart. The above example is good. The only - // startword that would be pushed to the array would be "Oh" and not "I'm", since we're not checking - // for where sentences get split up. - this.startwords.push(words[0]); - - // Now, we're going to iterate over all the words we've found in our currentLine, - // which is all the stuff we pushed in the new words array up above. - // Let's start adding real data to our dictionary! - for (var j = 0; j < words.length - 1; j++) { - - // TODO: I forget what this does... - var checkValid = true; // Flag to check whether a value is true or false. - - // This specifically checks if the current word is a hashtag, - // so that we can add it to a special array for later use. - // For example, maybe we'll want to attach completely random hashtags - // to our sentences. #blessed - if (words[j].substring(0, 1) === "#") { - var tempHashtag = words[j]; - tempHashtag = tempHashtag.replace(/[\W]*$/g,''); // Remove any cruft found at end of hashtag. - this.hashtags.push(tempHashtag); - } - - // Make sure our word isn't an empty value. No one likes that. Not even you. - if (words[j] !== '' && checkValid === true) { - - // New method for tracking words... - // TODO: This is a work in progress to improve how we're storing and - // referencing our word dictionary. WIP for v.2.0.0 - // Check if the word even exists in our array. - // If not, let's add it and then build in our template. - if (!this.dictionary[words[j]]) { - countWords++; - this.dictionary[words[j]] = { - count: 1, - next_words: [], - prev_words: [], - }; - } else { - // Word already exists in our dictionary. Let's update some stuff! - this.dictionary[words[j]].count++; - this.dictionary[words[j]].next_words.push(this.checkExists(words[j+1])); - this.dictionary[words[j]].prev_words.push(this.checkExists(words[j-1])); - } - - // NOTE: This is the current way we're storing data in our word dictionary. - // We simply add this object to an array. This means multiple objects will exist - // that feature the same object. It's really inefficient and long term, I want to - // improve how this works. - var curWord = { - first_word: words[j], - word_pair: words[j] + ' ' + this.checkExists(words[j+1]), - word_pair_array: [words[j], this.checkExists(words[j+1])], - next_word: this.checkExists(words[j+2]), - prev_word: this.checkExists(words[j-1]), - //count: 1, // Deprecated: Was originally using this to potentially rank keywords, but this isn't needed now. + this.dictionary = {}; // This is a new object I'm using to generate sentences in a more efficient manner than the wordpairs array. + this.startwords = []; // Words that start a sentence. + this.stopwords = []; // Stop words. + this.hashtags = []; // Automatically detect any hashtags that we use. + this.wordpairs = []; // This is where we'll store our dictionary of word pairs + this.popularKeywords = []; // Track popular keywords that the bot may be interested in based on our tweet history. +} + +// Build out initial word pair dictionary for creating +// semi-intelligent sounding sentences pseudo Markov chains. +Generator.prototype.buildCorpus = function(content) { + var countWords = 0; + content = this.cleanContent(content); + + // This for-loop will iterate over every single line of text that's passed in from the "content" argument. + // This will be text that's cleaned up and formatted correctly after the server / bot reads data from + // our tweets.txt file. In the case below, content[currentLine] will represent one line of text. + // Example: content[currentLine] === "Oh, man. I'm really hungry!" + for (var currentLine = 0; currentLine < content.length; currentLine++) { + + // In order to start properly building our corpus of processed text, + // we're going to need to split up each word in our sentence individually into an array. + // Since we're splitting on spaces between words, this will attach punctuation marks and the like. + // This is something we actually want! We can check for "end" words and stuff later. + // Example: ['Oh,', 'man.', 'I\'m', 'really', 'hungry!'] + var words = content[currentLine].split(' '); + + // We want our robot to sound intelligent, we track words that start each sentence (new line). + // There are some cases where this currently falls apart. The above example is good. The only + // startword that would be pushed to the array would be "Oh" and not "I'm", since we're not checking + // for where sentences get split up. + this.startwords.push(words[0]); + + // Now, we're going to iterate over all the words we've found in our currentLine, + // which is all the stuff we pushed in the new words array up above. + // Let's start adding real data to our dictionary! + for (var j = 0; j < words.length - 1; j++) { + + // TODO: I forget what this does... + var checkValid = true; // Flag to check whether a value is true or false. + + // This specifically checks if the current word is a hashtag, + // so that we can add it to a special array for later use. + // For example, maybe we'll want to attach completely random hashtags + // to our sentences. #blessed + if (words[j].substring(0, 1) === "#") { + var tempHashtag = words[j]; + tempHashtag = tempHashtag.replace(/[\W]*$/g,''); // Remove any cruft found at end of hashtag. + this.hashtags.push(tempHashtag); + } + + // Make sure our word isn't an empty value. No one likes that. Not even you. + if (words[j] !== '' && checkValid === true) { + + // New method for tracking words... + // TODO: This is a work in progress to improve how we're storing and + // referencing our word dictionary. WIP for v.2.0.0 + // Check if the word even exists in our array. + // If not, let's add it and then build in our template. + if (!this.dictionary[words[j]]) { + countWords++; + this.dictionary[words[j]] = { + count: 1, + next_words: [], + prev_words: [], }; - //countWords++; - this.wordpairs.push(curWord); + } else { + // Word already exists in our dictionary. Let's update some stuff! + this.dictionary[words[j]].count++; + this.dictionary[words[j]].next_words.push(this.checkExists(words[j+1])); + this.dictionary[words[j]].prev_words.push(this.checkExists(words[j-1])); } - } - } - - // Clean up the results by moving some undesirable ish from our word arrays. - delete this.startwords['"']; - delete this.startwords['']; - delete this.startwords[' ']; - - //console.log('STARTWORDS: ', this.startwords); - //console.log(this.wordpairs); - //console.log('TOTAL WORDS: ', countWords); - //console.log('DICTIONARY: ', this.dictionary); - return this.wordpairs; - }, - - // Checks if the next word exists and adds it to the corpus dictionary. - // TODO: We're probably returning unnecessary space if a word doesn't exist. Need to fix. - checkExists: function(value) { - if (!value) { - //return ''; - } else { - return value; - } - }, - checkSentenceEnd: function(word) { - - // Sometimes, an undefined value is passed in here and we need to properly handle it. - // Let's just return from the function and do nothing. - if (word === undefined) { - return false; - } - - var endMarks = ['.', '!', '?']; - var endMark = word.slice(-1); - if (endMarks.indexOf(endMark) != -1) { - return true; - } else { - return false; - } - }, - - // Supply an array and randomly pick a word. - choice: function (array) { - var randomWord = array[Math.floor(Math.random() * array.length)]; - //console.log(randomWord); - return randomWord; - }, - - choosePairs: function (firstWord, secondWord) { - var allResults = []; - var resultWordPair; - var getResult; - if (secondWord === undefined || secondWord === null) { - getResult = this.searchObject(this.wordpairs, 'first_word', firstWord); - resultWordPair = getResult[Math.floor(Math.random() * getResult.length)]; - - //Trying to check for a weird undefined error that sometimes happens and crashes app: - if (typeof(resultWordPair) == "undefined") { - //console.log('\n--== ERROR: No result returned... ==--\n') - allResults[0] = ''; - allResults[1] = ''; - allResults[2] = ''; - allResults[3] = 'end'; - return allResults; + // NOTE: This is the current way we're storing data in our word dictionary. + // We simply add this object to an array. This means multiple objects will exist + // that feature the same object. It's really inefficient and long term, I want to + // improve how this works. + var curWord = { + first_word: words[j], + word_pair: words[j] + ' ' + this.checkExists(words[j+1]), + word_pair_array: [words[j], this.checkExists(words[j+1])], + next_word: this.checkExists(words[j+2]), + prev_word: this.checkExists(words[j-1]), + //count: 1, // Deprecated: Was originally using this to potentially rank keywords, but this isn't needed now. + }; + //countWords++; + this.wordpairs.push(curWord); + } } - - allResults[0] = this.checkExists(resultWordPair.word_pair_array[0]); - allResults[1] = this.checkExists(resultWordPair.word_pair_array[1]); - allResults[2] = this.checkExists(resultWordPair.next_word); - - return allResults; - } else if (secondWord === '') { - // This means the second word does not exist. Uh, oh! - //console.log('--== Second word pair not detected. ==--'); + } + + // Clean up the results by moving some undesirable ish from our word arrays. + delete this.startwords['"']; + delete this.startwords['']; + delete this.startwords[' ']; + + // console.log('STARTWORDS: ', this.startwords.length); + // console.log(this.wordpairs); + console.log('TOTAL WORDS: ', countWords); + // console.log('DICTIONARY: ', this.dictionary); + return this.wordpairs; + +} + +// Checks if the next word exists and adds it to the corpus dictionary. +// TODO: We're probably returning unnecessary space if a word doesn't exist. Need to fix. +Generator.prototype.checkExists = function(value) { + if (!value) { + //return ''; + } else { + return value; + } +}; + + +Generator.prototype.checkSentenceEnd = function(word) { + // Sometimes, an undefined value is passed in here and we need to properly handle it. + // Let's just return from the function and do nothing. + if (word === undefined) { + return false; + } + + var endMarks = ['.', '!', '?']; + var endMark = word.slice(-1); + if (endMarks.indexOf(endMark) != -1) { + return true; + } else { + return false; + } +}; + +// Supply an array and randomly pick a word. +Generator.prototype.choice = function (array) { + var randomWord = array[Math.floor(Math.random() * array.length)]; + //console.log(randomWord); + return randomWord; +}; + + +Generator.prototype.choosePairs = function (firstWord, secondWord) { + var allResults = []; + var resultWordPair; + var getResult; + if (secondWord === undefined || secondWord === null) { + getResult = this.searchObject(this.wordpairs, 'first_word', firstWord); + resultWordPair = getResult[Math.floor(Math.random() * getResult.length)]; + + //Trying to check for a weird undefined error that sometimes happens and crashes app: + if (typeof(resultWordPair) == "undefined") { + //console.log('\n--== ERROR: No result returned... ==--\n') allResults[0] = ''; allResults[1] = ''; allResults[2] = ''; - allResults[3] = 'end'; // Send a flag to our sentence generation function that says no more words are detected, so stop. + allResults[3] = 'end'; return allResults; - } else { - getResult = this.searchObject(this.wordpairs, 'word_pair', firstWord + ' ' + secondWord); // Change I to whatever - resultWordPair = getResult[Math.floor(Math.random() * getResult.length)]; - - //Trying to check for a weird undefined error that sometimes happens and crashes app: - if (typeof(resultWordPair) == "undefined") { - //console.log('\n--== ERROR: No result returned... ==--\n') - allResults[0] = ''; - allResults[1] = ''; - allResults[2] = ''; - allResults[3] = 'end'; - return allResults; - } - - allResults[0] = this.checkExists(resultWordPair.word_pair_array[0]); - allResults[1] = this.checkExists(resultWordPair.word_pair_array[1]); - allResults[2] = this.checkExists(resultWordPair.next_word); - - return allResults; - } - }, - - // Clean up our content and remove things that result in poorly generated sentences. - cleanContent: function(content) { - var cleaned = content; - - cleaned.forEach(function(element, index) { - // Removing all sorts of weird content found in my tweets that screw this whole process up. - // Really, I should just get better at RegEx - cleaned[index] = cleaned[index].replace(/(@\S+)/gi,''); // Try to remove any usernames - cleaned[index] = cleaned[index].replace(/(http\S+)/gi,''); // Try to remove any URLs - cleaned[index] = cleaned[index].replace(/^RT\W/gi,''); // Remove "RT" -- though we're keeping the rest of the tweet. Should probably fix. - cleaned[index] = cleaned[index].replace(/( RT )/gi,' '); // Remove "RT" -- though we're keeping the rest of the tweet. Should probably fix. - cleaned[index] = cleaned[index].replace(/( MT )/g,' '); // Remove "MT" -- though we're keeping the rest of the tweet. Should probably fix. - cleaned[index] = cleaned[index].replace(/^ +/gm, ''); // Remove any leading whitespace - cleaned[index] = cleaned[index].replace(/[ \t]+$/, ''); // Remove any trailing whitespace - cleaned[index] = cleaned[index].replace(/(’)/, '\''); // Convert HTML entity to apostrophe - cleaned[index] = cleaned[index].replace(/(‘)/, '\''); // Convert HTML entity to apostrophe - cleaned[index] = cleaned[index].replace(/\W-$/g, ''); // Remove dashes at the end of a line that result from stripped URLs. - cleaned[index] = cleaned[index].replace(/>/g, '>'); // Convert greater than signs - cleaned[index] = cleaned[index].replace(/</g, '<'); // Convert less than signs - cleaned[index] = cleaned[index].replace(/&/g,'&'); // Convert HTML entity - cleaned[index] = cleaned[index].replace(/(\/cc)/gi, ''); // Remove "/cc" from tweets - cleaned[index] = cleaned[index].replace(/(\/via)/gi, ''); // Remove "/via" from tweets - cleaned[index] = cleaned[index].replace(/"/g, ''); // Remove quotes - cleaned[index] = cleaned[index].replace(/“/g, ''); // Remove quotes - cleaned[index] = cleaned[index].replace(/”/g, ''); // Remove quotes - cleaned[index] = cleaned[index].replace(/(\))/g, ''); // Hopefully remove parentheses found at end of a word, but not in emojis - cleaned[index] = cleaned[index].replace(/(\()/g, ''); // Hopefully remove parentheses found at the beginning of a word, but not in emojis - cleaned[index] = cleaned[index].replace(/(\\n)/gm,''); // Replace all commas in words with nothing. - //cleaned[index] = cleaned[index].replace(/(\...)/g,'…'); // Save characters and replace three periods… - //cleaned[index] = cleaned[index].replace(/[\(]/g, ''); // Remove quotes TODO: figure out how to get rid of these without destroying emojis. - }); - - return cleaned; - }, - - makeTweet: function (min_length) { - if (this.startwords === undefined || this.startwords.length === 0) { - return; - } - - var keepGoing = true; // Basically, we want to keep generating a sentence until we either run out of words or hit a punctuation mark. - var startWord = this.choice(this.startwords); // Get initial start word. - - var initialWords = this.choosePairs(startWord); // Choose initial word pair. - - var tweet = [startWord]; - tweet.push(initialWords[1]); - tweet.push(initialWords[2]); - - while (keepGoing === true) { - var getNewWords = this.choosePairs(tweet[tweet.length - 2],tweet[tweet.length - 1]); - if (getNewWords[3] === 'end') break; // No more words detected. Stop, yo! - - tweet.push(getNewWords[2]); - if (this.checkSentenceEnd(getNewWords[2]) === true) break; // Check if the end of the word contains a sentence ending element. } - // Remove undesireable elements from our array - var removeElements = function(array, value) { - if (array.indexOf(value) !== -1) { - //console.log('REMOVING: ' + value); - for(var i = array.length-1; i--;){ - if (array[i] === value) array.splice(i, 1); - } - } - return array; - }; - - // Try to remove some random crap that manages to sneak through my earlier filters. - // I need to be better at regex... - tweet = removeElements(tweet, '.'); - tweet = removeElements(tweet, '-'); - tweet = removeElements(tweet, 'RT'); - tweet = removeElements(tweet, '/'); - tweet = removeElements(tweet, ':'); - tweet = removeElements(tweet, '[pic]:'); - tweet = removeElements(tweet, '[pic]'); - - // Filter our array of words to remove ALL empty values ("", null, undefined and 0): - tweet = tweet.filter(function(e){return e;}); - - //console.log(tweet); - var wholeTweet = tweet.join(' '); - - // Clean up any whitespace added attached to the end up the tweet. - wholeTweet = wholeTweet.replace(/[ ]*$/g,''); - - // Sometimes, we get erroneous colons at the end of our sentences. Let's remove them. - // e.g., 'This is happening right now:' - wholeTweet = wholeTweet.replace(/:$/g,'.'); // Remove colon if found at end of line. - wholeTweet = wholeTweet.replace(/\,[ ]*$/g,'.'); // Remove comma if found at end of line. - wholeTweet = wholeTweet.replace(/[ ](w\/)$/g,''); // Remove '/w' that sometimes gets attached to end of lines. - - // For some reason, sometimes our sentence generation returns nothing. - // Detect if this happens and rerun the script again. - if (wholeTweet.length === 0) { - wholeTweet = this.makeTweet(min_length); + allResults[0] = this.checkExists(resultWordPair.word_pair_array[0]); + allResults[1] = this.checkExists(resultWordPair.word_pair_array[1]); + allResults[2] = this.checkExists(resultWordPair.next_word); + + return allResults; + } else if (secondWord === '') { + // This means the second word does not exist. Uh, oh! + //console.log('--== Second word pair not detected. ==--'); + allResults[0] = ''; + allResults[1] = ''; + allResults[2] = ''; + allResults[3] = 'end'; // Send a flag to our sentence generation function that says no more words are detected, so stop. + return allResults; + } else { + getResult = this.searchObject(this.wordpairs, 'word_pair', firstWord + ' ' + secondWord); // Change I to whatever + resultWordPair = getResult[Math.floor(Math.random() * getResult.length)]; + + //Trying to check for a weird undefined error that sometimes happens and crashes app: + if (typeof(resultWordPair) == "undefined") { + //console.log('\n--== ERROR: No result returned... ==--\n') + allResults[0] = ''; + allResults[1] = ''; + allResults[2] = ''; + allResults[3] = 'end'; + return allResults; } - return wholeTweet; - }, - - // I'll be honest. I kind of forget what this method does. - searchObject: function (array, prop, value) { - //console.log('SEARCH OBJECT??', array, prop, value); - var result = array.filter(function (obj) { - return obj[prop] === value; - }); - - //console.log('SEARCH OBJECT VALUE:', value); - //console.log('SEARCH OBJECT RESULTS:', result); - return result; - }, - - // Find all keywords in our word pair dictionary - getKeywords: function(word) { - var checkStopword = word.toLowerCase().replace(/[\W]*$/g,''); // Remove any cruft found at end of word. - - // Before we begin, let's check if the word is a stopword found in - // our stopwords.txt file. If so, we're going to ignore it. - if (this.stopwords.indexOf(checkStopword) == -1 && word !== '') { - var result = this.wordpairs.filter(function( obj ) { - //tempIndex = wordpairs.indexOf(obj); - //console.log(wordpairs.indexOf(obj)); - return obj.first_word == word; - }); - - //console.log('Total results: ' + result.length); - //console.log(result); - - return [result.length, word, result]; - } else { - //console.log('Word in stopword list: ' + word); - return ''; + allResults[0] = this.checkExists(resultWordPair.word_pair_array[0]); + allResults[1] = this.checkExists(resultWordPair.word_pair_array[1]); + allResults[2] = this.checkExists(resultWordPair.next_word); + + return allResults; + } +}; + +// Clean up our content and remove things that result in poorly generated sentences. +Generator.prototype.cleanContent = function(content) { + var cleaned = content; + + cleaned.forEach(function(element, index) { + // Removing all sorts of weird content found in my tweets that screw this whole process up. + // Really, I should just get better at RegEx + cleaned[index] = cleaned[index].replace(/(@\S+)/gi,''); // Try to remove any usernames + cleaned[index] = cleaned[index].replace(/(http\S+)/gi,''); // Try to remove any URLs + cleaned[index] = cleaned[index].replace(/^RT\W/gi,''); // Remove "RT" -- though we're keeping the rest of the tweet. Should probably fix. + cleaned[index] = cleaned[index].replace(/( RT )/gi,' '); // Remove "RT" -- though we're keeping the rest of the tweet. Should probably fix. + cleaned[index] = cleaned[index].replace(/( MT )/g,' '); // Remove "MT" -- though we're keeping the rest of the tweet. Should probably fix. + cleaned[index] = cleaned[index].replace(/^ +/gm, ''); // Remove any leading whitespace + cleaned[index] = cleaned[index].replace(/[ \t]+$/, ''); // Remove any trailing whitespace + cleaned[index] = cleaned[index].replace(/(’)/, '\''); // Convert HTML entity to apostrophe + cleaned[index] = cleaned[index].replace(/(‘)/, '\''); // Convert HTML entity to apostrophe + cleaned[index] = cleaned[index].replace(/\W-$/g, ''); // Remove dashes at the end of a line that result from stripped URLs. + cleaned[index] = cleaned[index].replace(/>/g, '>'); // Convert greater than signs + cleaned[index] = cleaned[index].replace(/</g, '<'); // Convert less than signs + cleaned[index] = cleaned[index].replace(/&/g,'&'); // Convert HTML entity + cleaned[index] = cleaned[index].replace(/(\/cc)/gi, ''); // Remove "/cc" from tweets + cleaned[index] = cleaned[index].replace(/(\/via)/gi, ''); // Remove "/via" from tweets + cleaned[index] = cleaned[index].replace(/"/g, ''); // Remove quotes + cleaned[index] = cleaned[index].replace(/“/g, ''); // Remove quotes + cleaned[index] = cleaned[index].replace(/”/g, ''); // Remove quotes + cleaned[index] = cleaned[index].replace(/(\))/g, ''); // Hopefully remove parentheses found at end of a word, but not in emojis + cleaned[index] = cleaned[index].replace(/(\()/g, ''); // Hopefully remove parentheses found at the beginning of a word, but not in emojis + cleaned[index] = cleaned[index].replace(/(\\n)/gm,''); // Replace all commas in words with nothing. + //cleaned[index] = cleaned[index].replace(/(\...)/g,'…'); // Save characters and replace three periods… + //cleaned[index] = cleaned[index].replace(/[\(]/g, ''); // Remove quotes TODO: figure out how to get rid of these without destroying emojis. + }); + + return cleaned; +}; + +Generator.prototype.makeTweet = function (min_length) { + if (this.startwords === undefined || this.startwords.length === 0) { + return; + } + + var keepGoing = true; // Basically, we want to keep generating a sentence until we either run out of words or hit a punctuation mark. + var startWord = this.choice(this.startwords); // Get initial start word. + + var initialWords = this.choosePairs(startWord); // Choose initial word pair. + + var tweet = [startWord]; + tweet.push(initialWords[1]); + tweet.push(initialWords[2]); + + while (keepGoing === true) { + var getNewWords = this.choosePairs(tweet[tweet.length - 2],tweet[tweet.length - 1]); + if (getNewWords[3] === 'end') break; // No more words detected. Stop, yo! + + tweet.push(getNewWords[2]); + if (this.checkSentenceEnd(getNewWords[2]) === true) break; // Check if the end of the word contains a sentence ending element. + } + + // Remove undesireable elements from our array + var removeElements = function(array, value) { + if (array.indexOf(value) !== -1) { + //console.log('REMOVING: ' + value); + for(var i = array.length-1; i--;){ + if (array[i] === value) array.splice(i, 1); } - }, - - makeSentenceFromKeyword: function(replystring) { - var allWords = []; // Our array of all words. - var mySentence = []; // Store words we find in this array. - allWords = replystring.split(' '); - - // Pass in proper context to the calculateHighest function - var self = this; - - // Calculate highest keyword - function calculateHighest(allWords) { - var count = 0; - var highestWord; - var resultArray = []; - - for (var i = 0; i < allWords.length; i++) { - //console.log('Getting results for: ' + feederArray[i]); - var result = self.getKeywords(allWords[i]); - if (result[0] > count) { - count = result[0]; - highestWord = result[1]; - resultArray = result[2]; - } + } + return array; + }; + + // Try to remove some random crap that manages to sneak through my earlier filters. + // I need to be better at regex... + tweet = removeElements(tweet, '.'); + tweet = removeElements(tweet, '-'); + tweet = removeElements(tweet, 'RT'); + tweet = removeElements(tweet, '/'); + tweet = removeElements(tweet, ':'); + tweet = removeElements(tweet, '[pic]:'); + tweet = removeElements(tweet, '[pic]'); + + // Filter our array of words to remove ALL empty values ("", null, undefined and 0): + tweet = tweet.filter(function(e){return e;}); + + //console.log(tweet); + var wholeTweet = tweet.join(' '); + + // Clean up any whitespace added attached to the end up the tweet. + wholeTweet = wholeTweet.replace(/[ ]*$/g,''); + + // Sometimes, we get erroneous colons at the end of our sentences. Let's remove them. + // e.g., 'This is happening right now:' + wholeTweet = wholeTweet.replace(/:$/g,'.'); // Remove colon if found at end of line. + wholeTweet = wholeTweet.replace(/\,[ ]*$/g,'.'); // Remove comma if found at end of line. + wholeTweet = wholeTweet.replace(/[ ](w\/)$/g,''); // Remove '/w' that sometimes gets attached to end of lines. + + // For some reason, sometimes our sentence generation returns nothing. + // Detect if this happens and rerun the script again. + if (wholeTweet.length === 0) { + wholeTweet = this.makeTweet(min_length); + } + + return wholeTweet; +} + +// I'll be honest. I kind of forget what this method does. +Generator.prototype.searchObject = function (array, prop, value) { + //console.log('SEARCH OBJECT??', array, prop, value); + var result = array.filter(function (obj) { + return obj[prop] === value; + }); + + //console.log('SEARCH OBJECT VALUE:', value); + //console.log('SEARCH OBJECT RESULTS:', result); + return result; +}; + +// Find all keywords in our word pair dictionary +Generator.prototype.getKeywords = function(word) { + var checkStopword = word.toLowerCase().replace(/[\W]*$/g,''); // Remove any cruft found at end of word. + + // Before we begin, let's check if the word is a stopword found in + // our stopwords.txt file. If so, we're going to ignore it. + if (this.stopwords.indexOf(checkStopword) == -1 && word !== '') { + var result = this.wordpairs.filter(function( obj ) { + //tempIndex = wordpairs.indexOf(obj); + //console.log(wordpairs.indexOf(obj)); + return obj.first_word == word; + }); + + //console.log('Total results: ' + result.length); + //console.log(result); + + return [result.length, word, result]; + } else { + //console.log('Word in stopword list: ' + word); + return ''; + } +}; + +Generator.prototype.makeSentenceFromKeyword = function(replystring) { + var allWords = []; // Our array of all words. + var mySentence = []; // Store words we find in this array. + allWords = replystring.split(' '); + + // Pass in proper context to the calculateHighest function + var self = this; + + // Calculate highest keyword + function calculateHighest(allWords) { + var count = 0; + var highestWord; + var resultArray = []; + + for (var i = 0; i < allWords.length; i++) { + //console.log('Getting results for: ' + feederArray[i]); + var result = self.getKeywords(allWords[i]); + if (result[0] > count) { + count = result[0]; + highestWord = result[1]; + resultArray = result[2]; } - - //console.log('\nHighest ranked word in corpus is: \'' + highestWord + '\' and was found ' + count + ' times.'); - //console.log(resultArray); - return resultArray; } - //console.log('Testing ranking:'); - //console.log(calculateHighest(allWords)); - - var keywordObject = calculateHighest(allWords); - - var keepGoing = true; // Keep generating our sentence until we no longer have to. - //console.log(obj); - // Choose random result - var result = keywordObject[Math.floor(Math.random() * keywordObject.length)]; - - //console.log(result); - - // Error checking to handle undefined / unfound words. - if (typeof result == 'undefined') { - //console.log('\nError: No matching keywords found.'); - return; - } + //console.log('\nHighest ranked word in corpus is: \'' + highestWord + '\' and was found ' + count + ' times.'); + //console.log(resultArray); + return resultArray; + } - var prev_word = result.prev_word; - var cur_word = result.first_word; + //console.log('Testing ranking:'); + //console.log(calculateHighest(allWords)); - // Add intial words to array. - mySentence.push(prev_word, cur_word); + var keywordObject = calculateHighest(allWords); - // First part of our "Build Sentence from Keyword" Function - // This generates everything BEFORE our keyword. - while (keepGoing === true) { - var cur_wordpair = mySentence[0] + ' ' + mySentence[1]; - var tempArray = this.chooseRandomPair(this.findWordPair(cur_wordpair)); + var keepGoing = true; // Keep generating our sentence until we no longer have to. + //console.log(obj); + // Choose random result + var result = keywordObject[Math.floor(Math.random() * keywordObject.length)]; - // Check if an error condition exists and end things - if (tempArray[3] == 'notfound') { - console.log('\nError: No keyword pairs found'); - return; - } + //console.log(result); - if (tempArray[0] === '') { - keepGoing = false; - } else { - mySentence.unshift(tempArray[0]); - } - } + // Error checking to handle undefined / unfound words. + if (typeof result == 'undefined') { + //console.log('\nError: No matching keywords found.'); + return; + } - // Second part of our "Build Sentence from Keyword" Function - // This generates everything AFTER our keyword. - keepGoing = true; // Reset our keep going variable. - while (keepGoing === true) { - var arrayLength = mySentence.length - 1; - var cur_wordpair = mySentence[arrayLength - 1] + ' ' + mySentence[arrayLength]; - var tempArray = this.chooseRandomPair(this.findWordPair(cur_wordpair)); - - // Check if an error condition exists and end things - if (tempArray[3] == 'notfound') { - console.log('\nError: No keyword pairs found'); - return; - } + var prev_word = result.prev_word; + var cur_word = result.first_word; - if (tempArray[2] === '') { - keepGoing = false; - } else { - mySentence.push(tempArray[2]); - } - } + // Add intial words to array. + mySentence.push(prev_word, cur_word); - // Run this again until we have a sentence under 124 characters. - // This is because the max length of Twitter username is 15 characters + 1 space. - // TODO: Imporve this so we can count the username we're replying to. + // First part of our "Build Sentence from Keyword" Function + // This generates everything BEFORE our keyword. + while (keepGoing === true) { + var cur_wordpair = mySentence[0] + ' ' + mySentence[1]; + var tempArray = this.chooseRandomPair(this.findWordPair(cur_wordpair)); - if (mySentence.join(' ').length > 124) { - makeSentenceFromKeyword(replystring); - } else { - // TODO: Better error handling when we return no object. - //console.log('\nGenerated response: ' + allWords.join(' ')); - //console.log('(' + allWords.join(' ').length + ' characters.)'); - - var returnSentence = mySentence.join(' '); - - if (typeof returnSentence == 'undefined') { - console.log('\nError: No valid replies found'); - return; - } else { - return mySentence.join(' '); - } + // Check if an error condition exists and end things + if (tempArray[3] == 'notfound') { + console.log('\nError: No keyword pairs found'); + return; } - }, - // Make sure our function spits out phrases less than a certain length. - twitterFriendly: function (reply, username) { - var new_tweet; - if (reply) { - username = '@' + username + ' '; + if (tempArray[0] === '') { + keepGoing = false; } else { - username = ''; - } - - do { - var randomLength = Math.floor((Math.random() * 20) + 10); // Random length between 10 and 20 words. - new_tweet = this.makeTweet(randomLength); - new_tweet = username + new_tweet + this.attachHashtag(new_tweet.length); // Randomly add a hashtag - new_tweet = new_tweet + this.attachEmoji(new_tweet.length); // Randomy add an emoji - - } while (new_tweet.length > 140); - - // TODO: This is a stupid, hacky way to fix weird messages that only say "RT" every so often. - if (new_tweet == "RT") { - twitterFriendly(reply, username); + mySentence.unshift(tempArray[0]); } - return new_tweet; - }, - - /******* - * - * BUILD SENTENCE FROM KEYWORD? - * - */ - chooseRandomPair: function (obj) { - - if (typeof obj === 'undefined') { - return ['']; + } + + // Second part of our "Build Sentence from Keyword" Function + // This generates everything AFTER our keyword. + keepGoing = true; // Reset our keep going variable. + while (keepGoing === true) { + var arrayLength = mySentence.length - 1; + var cur_wordpair = mySentence[arrayLength - 1] + ' ' + mySentence[arrayLength]; + var tempArray = this.chooseRandomPair(this.findWordPair(cur_wordpair)); + + // Check if an error condition exists and end things + if (tempArray[3] == 'notfound') { + console.log('\nError: No keyword pairs found'); + return; } - var result = obj[Math.floor(Math.random() * obj.length)]; - - // Check if any results are found. - if (typeof result === 'undefined') { - //console.log('Error: No object'); - var prev_word = ''; - var cur_word = ''; - var next_word = ''; - var error = 'notfound'; - - } else if (typeof result.prev_word !== 'undefined') { - var prev_word = result.prev_word; - var cur_word = result.first_word; - var next_word = result.next_word; + if (tempArray[2] === '') { + keepGoing = false; } else { - var prev_word = ''; + mySentence.push(tempArray[2]); } - + } + // Run this again until we have a sentence under 124 characters. + // This is because the max length of Twitter username is 15 characters + 1 space. + // TODO: Imporve this so we can count the username we're replying to. - // If we detect an end of sentence in previous word, let's just stop right there. - if (prev_word.slice(-1) == '.' || prev_word.slice(-1) == '!' || prev_word.slice(-1) == '?') { - //console.log('End sentence detected'); - prev_word = ''; - } - - // Returns the following array - // ['prev_word', 'first_word', 'next_word'] - return [prev_word, cur_word, next_word, error]; - }, - - findWordPair: function(string) { - var getResult = this.searchObject(this.wordpairs, 'word_pair', string); - //console.log( getResult ); - return getResult; - }, - - // Random add a hashtag to the end of our tweet. - attachHashtag: function(tweetlength) { - var gethashtag; - var x = Math.random(); // Generate random number to determine whether we add a hashtag or not - if (x <= config.personality.addHashtags) { - // Pick a random emoji from our array - gethashtag = this.hashtags[Math.floor(Math.random()*this.hashtags.length)]; - - // Fix error checking when hashtags might not exist. - if (typeof gethashtag == 'undefined') { - gethashtag = ''; - } + if (mySentence.join(' ').length > 124) { + makeSentenceFromKeyword(replystring); + } else { + // TODO: Better error handling when we return no object. + //console.log('\nGenerated response: ' + allWords.join(' ')); + //console.log('(' + allWords.join(' ').length + ' characters.)'); - // Check if we should be ignoring this hashtag before we include it. - if (config.personality.ignoredHashtags.indexOf(gethashtag.toLowerCase()) !== -1) { - //console.log('Ignoring the following hashtag: ' + gethashtag); - gethashtag = ''; - } else if (typeof gethashtag == 'undefined') { - console.log('\nUndefined hashtag detected'); - gethashtag = ''; - } else { - // Add padding to hashtag - gethashtag = ' ' + gethashtag; - } + var returnSentence = mySentence.join(' '); + if (typeof returnSentence == 'undefined') { + console.log('\nError: No valid replies found'); + return; } else { + return mySentence.join(' '); + } + } +}; + +// Make sure our function spits out phrases less than a certain length. +Generator.prototype.twitterFriendly = function (reply, username) { + var new_tweet; + if (reply) { + username = '@' + username + ' '; + } else { + username = ''; + } + + do { + var randomLength = Math.floor((Math.random() * 20) + 10); // Random length between 10 and 20 words. + new_tweet = this.makeTweet(randomLength); + new_tweet = username + new_tweet + this.attachHashtag(new_tweet.length); // Randomly add a hashtag + new_tweet = new_tweet + this.attachEmoji(new_tweet.length); // Randomy add an emoji + } while (new_tweet.length > 140); + + // TODO: This is a stupid, hacky way to fix weird messages that only say "RT" every so often. + if (new_tweet == "RT") { + twitterFriendly(reply, username); + } + return new_tweet; +}; + +/******* +* +* BUILD SENTENCE FROM KEYWORD? +* +*/ +Generator.prototype.chooseRandomPair = function (obj) { + + if (typeof obj === 'undefined') { + return ['']; + } + + var result = obj[Math.floor(Math.random() * obj.length)]; + + // Check if any results are found. + if (typeof result === 'undefined') { + //console.log('Error: No object'); + var prev_word = ''; + var cur_word = ''; + var next_word = ''; + var error = 'notfound'; + + } else if (typeof result.prev_word !== 'undefined') { + var prev_word = result.prev_word; + var cur_word = result.first_word; + var next_word = result.next_word; + } else { + var prev_word = ''; + } + + + + // If we detect an end of sentence in previous word, let's just stop right there. + if (prev_word.slice(-1) == '.' || prev_word.slice(-1) == '!' || prev_word.slice(-1) == '?') { + //console.log('End sentence detected'); + prev_word = ''; + } + + // Returns the following array + // ['prev_word', 'first_word', 'next_word'] + return [prev_word, cur_word, next_word, error]; +}; + +Generator.prototype.findWordPair = function(string) { + var getResult = this.searchObject(this.wordpairs, 'word_pair', string); + //console.log( getResult ); + return getResult; +}; + +// Random add a hashtag to the end of our tweet. +Generator.prototype.attachHashtag = function(tweetlength) { + var gethashtag; + var x = Math.random(); // Generate random number to determine whether we add a hashtag or not + if (x <= this.config.personality.addHashtags) { + // Pick a random emoji from our array + gethashtag = this.hashtags[Math.floor(Math.random()*this.hashtags.length)]; + + // Fix error checking when hashtags might not exist. + if (typeof gethashtag == 'undefined') { gethashtag = ''; - } - - - if (tweetlength < 120) { - return gethashtag; - } - }, - - // Let's randomly include an emoji at the end of the tweet. - attachEmoji: function(tweetlength) { - var emoji; - var emojis = [ - " 💩💩💩", - " 😍😍", - " 💩", - " 😐", - " 😝", - " 😖", - " 😎", - " 😘", - " 😍", - " 😄", - " 👍", - " 👎", - " 👊", - " 🌟", - " 🌟🌟🌟", - " 😵", - " 😡", - " 🙀", - " 🍺", - " ❤", - " 💔", - " 🏃💨 💩" - ]; - - var x = Math.random(); // Generate random number to determine whether we show emoji or not - if (x <= config.personality.addEmojis) { - // Pick a random emoji from our array - emoji = emojis[Math.floor(Math.random()*emojis.length)]; - } else { - emoji = ''; } - - if (tweetlength < 130) { - return emoji; + // Check if we should be ignoring this hashtag before we include it. + if (this.config.personality.ignoredHashtags.indexOf(gethashtag.toLowerCase()) !== -1) { + //console.log('Ignoring the following hashtag: ' + gethashtag); + gethashtag = ''; + } else if (typeof gethashtag == 'undefined') { + console.log('\nUndefined hashtag detected'); + gethashtag = ''; + } else { + // Add padding to hashtag + gethashtag = ' ' + gethashtag; } - }, -}; \ No newline at end of file + } else { + gethashtag = ''; + } + + + if (tweetlength < 120) { + return gethashtag; + } +}; + +// Let's randomly include an emoji at the end of the tweet. +Generator.prototype.attachEmoji = function(tweetlength) { + var emoji; + var emojis = [ + " 💩💩💩", + " 😍😍", + " 💩", + " 😐", + " 😝", + " 😖", + " 😎", + " 😘", + " 😍", + " 😄", + " 👍", + " 👎", + " 👊", + " 🌟", + " 🌟🌟🌟", + " 😵", + " 😡", + " 🙀", + " 🍺", + " ❤", + " 💔", + " 🏃💨 💩" + ]; + + var x = Math.random(); // Generate random number to determine whether we show emoji or not + if (x <= this.config.personality.addEmojis) { + // Pick a random emoji from our array + emoji = emojis[Math.floor(Math.random()*emojis.length)]; + } else { + emoji = ''; + } + + + if (tweetlength < 130) { + return emoji; + } +}; + + +module.exports = Generator; diff --git a/components/robot/index.js b/components/robot/index.js index 01e2915..dc1ae56 100644 --- a/components/robot/index.js +++ b/components/robot/index.js @@ -4,137 +4,142 @@ **/ // Import required npm modules to make our robot work! +var path = require('path'); var fs = require('fs'); var Promise = require('bluebird'); +fs = Promise.promisifyAll(fs); ////////// -var config = require('../../config'); // Robot config, personality settings and API keys -var generator = require('../generator'); // Compiles word dictionary and builds new sentences. -var tweet = require('../tweets'); // Methods to interact with Twitter by writing and favoriting tweets and more. +var Generator = require('../generator'); // Compiles word dictionary and builds new sentences. +var Tweet = require('../tweets'); // Methods to interact with Twitter by writing and favoriting tweets and more. var utils = require('../utilities'); // Various helper functions -// Create promises -fs = Promise.promisifyAll(fs); -generator = Promise.promisifyAll(generator); ////// Start up tasks ////// -// Process a provided list of stop words. -generator.stopwords = fs.readFileAsync('./data/stopwords.txt').toString().split("\n"); -// Filename to source or tweets and other content from? -tweetFile = './tweets.txt'; +var Robot = function Robot(config) { + this.generator = new Generator(config); + this.generator = Promise.promisifyAll(this.generator); + this.tweet = new Tweet(this.generator, config); + this.config = config; -// Track times of various actions from our robot -var robotActions = { - lastFollowCheck: 0, - lastRandomReply: 0, - lastReply: 0, - lastRetweet: 0, - lastTweet: 0 -}; + // Process a provided list of stop words. + this.generator.stopwords = fs.readFileAsync(path.join(__dirname, '../../data/stopwords.txt')).toString().split("\n"); -module.exports = { + // Filename to source or tweets and other content from? + tweetFile = this.config.settings.corpus || path.join(__dirname, '../../tweets.sample.txt'); + // Track times of various actions from our robot + this.robotActions = { + lastFollowCheck: 0, + lastRandomReply: 0, + lastReply: 0, + lastRetweet: 0, + lastTweet: 0 + }; // Initialize robot and start doing robot things. // Example, build word dictionary. Start watching stream, etc. - init: function() { - // Load up robot settings. - console.log('\n\nNODEEBOT FOR NODEJS v.0.2.0'); - console.log('by Dave Schumaker (@davely)\n'); - console.log('-== CONFIG SETTINGS ==-'); - console.log(' -Post to Twitter? ' + config.settings.postTweets); - console.log(' -Repond to DMs? ' + config.settings.respondDMs); - console.log(' -Repond to replies? ' + config.settings.respondReplies); - console.log(' -Random replies? ' + config.settings.randomReplies); - console.log(' -Follow new users? ' + config.settings.followUsers); - console.log(' -Mark tweets as favorites? ' + config.settings.canFavoriteTweets); - console.log(' -Tweet interval: ' + config.settings.postInterval + ' seconds'); - console.log('\nAnalyzing data and creating word corpus from file \'' + tweetFile + '\''); - console.log('(This may take a few minutes to generate...)'); - - // Set proper context - var self = this; - - // Load in text file containing raw Tweet data. - fs.readFileAsync(tweetFile) - .then(function(fileContents) { - //Split content into array, separating by line. - var content = fileContents.toString().split("\n"); - return content; - }) - .then(function(content){ - //Build word corpus using content array above. - return generator.buildCorpus(content); - }) - .then(function(data){ - // Once word dictionary is built, kick off the robots actions! - self.onBoot(); - - /* - * There may be a better way to handle this. Right now, - * this interval runs every 5 seconds and calls the - * robotTasks function, which handles the logic that checks - * if it's time to send a new tweet, reload the Twitter stream - * etc. - */ - setInterval(function() { - self.robotTasks(); - }, 5000); - }); - }, - - /* - * These are tasks the robot should do the first time it loads up. - */ - onBoot: function() { - // Start watching the Twitter stream. - tweet.watchStream(); - - // Check for new followers. - if (config.settings.followUsers) { - robotActions.lastFollowCheck = Math.floor(Date.now() / 1000); - tweet.getFollowers(); - } - - // Check if the robot is allowed to tweet on startup. - // If so, post a tweet! - if (config.settings.tweetOnStartup) { - robotActions.lastTweet = Math.floor(Date.now() / 1000); // Update time of the last tweet. - newTweet = generator.makeTweet(140); // Create a new tweet. - tweet.postNewTweet(newTweet); // Post tweet. - console.log(utils.currentTime(), newTweet + ''); - } - }, - - robotTasks: function() { - /* - * Check how long it's been since robot last tweeted. - * If amount of time is greater than the tweet interval - * defined in the config file. Go ahead and post a new tweet. - */ - if (Math.floor(Date.now() / 1000) - robotActions.lastTweet >= config.settings.postInterval) { - robotActions.lastTweet = Math.floor(Date.now() / 1000); // Update time of the last tweet. - newTweet = generator.makeTweet(140); // Create a new tweet. - tweet.postNewTweet(newTweet); // Post tweet. - console.log(utils.currentTime(), newTweet + ''); - } - - /* - * Check if the Twitter Stream dropped. If so, reinitialize it. - */ - tweet.checkStream(); - - /* - * Check for new followers - */ - if (config.settings.followUsers && Math.floor(Date.now() / 1000) - robotActions.lastFollowCheck >= 300) { - robotActions.lastFollowCheck = Math.floor(Date.now() / 1000); - tweet.getFollowers(); - } - } - -}; + + // Load up robot settings. + console.log('\n\nNODEEBOT FOR NODEJS v.0.2.0'); + console.log('by Dave Schumaker (@davely)\n'); + console.log('-== CONFIG SETTINGS ==-'); + console.log(' -Post to Twitter? ' + config.settings.postTweets); + console.log(' -Repond to DMs? ' + config.settings.respondDMs); + console.log(' -Repond to replies? ' + config.settings.respondReplies); + console.log(' -Random replies? ' + config.settings.randomReplies); + console.log(' -Follow new users? ' + config.settings.followUsers); + console.log(' -Mark tweets as favorites? ' + config.settings.canFavoriteTweets); + console.log(' -Tweet interval: ' + config.settings.postInterval + ' seconds'); + console.log('\nAnalyzing data and creating word corpus from file \'' + tweetFile + '\''); + console.log('(This may take a few minutes to generate...)'); + + // Set proper context + var self = this; + + // Load in text file containing raw Tweet data. + fs.readFileAsync(tweetFile) + .then(function(fileContents) { + //Split content into array, separating by line. + var content = fileContents.toString().split("\n"); + return content; + }) + .then(function(content){ + //Build word corpus using content array above. + return self.generator.buildCorpus(content); + }) + .then(function(data){ + // Once word dictionary is built, kick off the robots actions! + self.onBoot(); + + console.log('I\m alive and waiting for tasks!'); + + /* + * There may be a better way to handle this. Right now, + * this interval runs every 5 seconds and calls the + * robotTasks function, which handles the logic that checks + * if it's time to send a new tweet, reload the Twitter stream + * etc. + */ + setInterval(function() { + self.robotTasks(); + }, 5000); + }); + +} + +/* +* These are tasks the robot should do the first time it loads up. +*/ +Robot.prototype.onBoot = function() { + // Start watching the Twitter stream. + this.tweet.watchStream(); + + // Check for new followers. + if (this.config.settings.followUsers) { + this.robotActions.lastFollowCheck = Math.floor(Date.now() / 1000); + this.tweet.getFollowers(); + } + + // Check if the robot is allowed to tweet on startup. + // If so, post a tweet! + if (this.config.settings.tweetOnStartup) { + this.robotActions.lastTweet = Math.floor(Date.now() / 1000); // Update time of the last tweet. + newTweet = this.generator.makeTweet(140); // Create a new tweet. + this.tweet.postNewTweet(newTweet); // Post tweet. + console.log(utils.currentTime(), newTweet + ''); + } +} + +Robot.prototype.robotTasks = function() { + /* + * Check how long it's been since robot last tweeted. + * If amount of time is greater than the tweet interval + * defined in the config file. Go ahead and post a new tweet. + */ + if (Math.floor(Date.now() / 1000) - this.robotActions.lastTweet >= this.config.settings.postInterval) { + this.robotActions.lastTweet = Math.floor(Date.now() / 1000); // Update time of the last tweet. + newTweet = this.generator.makeTweet(140); // Create a new tweet. + this.tweet.postNewTweet(newTweet); // Post tweet. + console.log(utils.currentTime(), newTweet + ''); + } + + /* + * Check if the Twitter Stream dropped. If so, reinitialize it. + */ + this.tweet.checkStream(); + + /* + * Check for new followers + */ + if (this.config.settings.followUsers && Math.floor(Date.now() / 1000) - this.robotActions.lastFollowCheck >= 300) { + this.robotActions.lastFollowCheck = Math.floor(Date.now() / 1000); + this.tweet.getFollowers(); + } +} + +module.exports = Robot; ///// DEBUG STUFF @@ -144,4 +149,4 @@ module.exports = { // user: { // screen_name: 'fakeuser', // } -// }; \ No newline at end of file +// }; diff --git a/components/tweets/index.js b/components/tweets/index.js index 601b5b0..89b5b79 100644 --- a/components/tweets/index.js +++ b/components/tweets/index.js @@ -1,369 +1,373 @@ /** * Actions for watching and posting to Twitter - * + * */ var Promise = require('bluebird'); var Twitter = require('twitter'); -var config = require('../../config'); -var generator = require('../generator'); var utils = require('../utilities'); -// Initialize a new Twitter client using the provided API keys. -var client = new Twitter({ - consumer_key: config.twitter.consumer_key, - consumer_secret: config.twitter.consumer_secret, - access_token_key: config.twitter.access_token_key, - access_token_secret: config.twitter.access_token_secret -}); // On initial load, store robot's interests as an object for faster lookup. var interestsObject = {}; -module.exports = { - // Map robot's interests array to an object for constant time lookup. - mapInterests: function() { - config.personality.robotInterests.forEach(function(element) { - interestsObject[element] = true; - }); - }, - - // Initialize our Twitter stream and start monitoring - // new tweets that appear in our as they come in. - watchStream: function (mode) { - // Set proper context for 'this' since it will be called inside some functions. - var self = this; - - // Each time we call the watchStream method (usually on bootup), map - // robot interests array from config component into a local object. - this.mapInterests(); - - if (config.settings.monitorStream) { - client.stream('user', function(stream){ - console.log('Listening to stream...\n\n'); - - // Start streaming data from Twitter. - stream.on('data', function(tweet) { - // Update time of last tweet that we received so we can check if we've dropped the streaming connection. - config.settings.lastTweetReceivedTime = Math.floor(Date.now() / 1000); - - // This handles actions that involve a tweet being deleted. If so, we don't want to call the stuff below. - if (tweet.delete || tweet.friends) { - //console.log('Detected deletion request from Twitter API...'); - } else { - // Look at contents of the tweet and determine if we should favorite it. - if (config.settings.canFavoriteTweets) { - self.checkInterests(tweet); // Potentially favorite tweet based on interests of our robot. - } - - // Look at Tweet and determine if it's a reply to our robot. - if (tweet.id !== null) { - self.checkReply(tweet); - } - //console.log(tweet); - } - }); - - // Check if we want to destroy the stream - // I don't think this is really working at the moment. - if (mode === 'destroy') { - config.settings.monitorStream = false; - //stream.destroy(); - //return; - } - - stream.on('error', function(error) { - console.log('Error detected with streaming API.'); - console.log(error); - }); - }); - } else if (!config.settings.monitorStream) { - console.log('\nWarning: Monitoring Twitter stream is currently disabled \ndue to configuration setting.'); - } else { - console.log('\nWarning: Something happened while trying watch the Twitter Stream and I\'m not sure what it is.'); - } - }, - - // Count the number of times we've recently replied to a user. - countReplies: function(user) { - var countReplies = config.settings.trackReplies.reduce(function(n, val) { - return n + (val === user); - }, 0); - - //console.log(trackReplies); - - if (countReplies >= 5) { - console.log("Warning: Replies to @" + user + " are Temporarily paused so we don't spam them."); - } - - return countReplies; - }, - - // Check if a new tweet in our stream is a reply to us. - checkReply: function(tweet) { - var checkTweet = tweet.text.toLowerCase(); - var myUsername = '@' + config.settings.robotName.toLowerCase(); - //var randomReply = false; // Initial condition that makes bot decide whether or not to reply to someone in its feed. - - var replyID = tweet.id_str; // Get the ID of the tweet so we can properly reply to it. - var replyUsername = tweet.user.screen_name; - var replyCount = 0; - - // Randomly reply to tweets that pop up in our stream. - var x = Math.random(); // Generate random number to determine whether we will reply or not - if (config.settings.randomReplies && x <= config.settings.randomRepliesChance && checkTweet.indexOf(myUsername) == -1 && (typeof tweet.retweeted_status === "undefined")) { - - //replyCount = this.countReplies(replyUsername); // Get the number of times we've recently replied to this user. - - // Prevent robot from going into a reply loop with itself and imploding the universe! - if (replyUsername.toLowerCase() !== config.settings.robotName.toLowerCase() && replyCount < 5) { - config.settings.trackReplies.push(replyUsername); // Add user to our reply tracker so we don't spam them too much. - console.log('\nRandomly replying to the following tweet from @' + replyUsername + ':'); - console.log(tweet.text); - - - this.writeReply(replyUsername, replyID, tweet.text); - } - } - - // Check if the tweet is a retweet so we don't gum up our replies with crazy username garbage. - if (typeof tweet.retweeted_status !== "undefined") { - //console.log("\n\n\n!!!!!!!!!!!! ALERT: RETWEET!!!!!\n\n\n"); - } - - // Build a standard reply to a user who mentions us. - if (checkTweet.indexOf(myUsername) != -1 && (typeof tweet.retweeted_status == "undefined")) { - var tempUserArray = []; // Keep track of all the users mentioned in this tweet. - var tempAdditionalUsers; // We'll use this to generate a string of additional users. - - // Checks tweet for any additional mentions and adds them to a temporary array. - var checkMentions = tweet.text.split(' '); - for (var i = 0; i < checkMentions.length; i++) { - if (checkMentions[i].substring(0, 1) == "@") { - // Make sure we aren't adding our own robot to the array - if (checkMentions[i].toLowerCase() != '@' + config.settings.robotName.toLowerCase()) { - checkMentions[i] = checkMentions[i].replace(/[\W]*$/g,''); // Remove any cruft at end of username like question marks. - //console.log('Found additional user mentioned: ' + checkMentions[i]); - tempUserArray.push(checkMentions[i]); - } - } - } - - // See if we have any additional users that we'll pass to our reply function below. - var replyUsers; - if (tempUserArray.length > 0) { - tempAdditionalUsers = tempUserArray.join(' '); - replyUsers = replyUsername + ' ' + tempAdditionalUsers; - } else { - replyUsers = replyUsername; - } - - //replyCount = this.countReplies(replyUsername); // Get the number of times we've recently replied to this user. - - // Prevent robot from going into a reply loop with itself! - if (replyUsername.toLowerCase() !== config.settings.robotName.toLowerCase() && replyCount < 5) { - // Quick and dirty way to add any bots to our temporary replies blacklist. - if (config.personality.otherBots.indexOf(replyUsername.toLowerCase()) != -1) { - console.log('User \'' + replyUsername +'\' is in our bot list. Temporarily limiting replies to this user.'); - //config.settings.trackReplies.push(replyUsername,replyUsername,replyUsername,replyUsername,replyUsername,replyUsername); - } - - //config.settings.trackReplies.push(replyUsername); // Add user to our reply tracker so we don't spam them too much. - console.log('\nNew reply from @' + replyUsername + ':'); - console.log(tweet.text); - - this.writeReply(replyUsers, replyID, tweet.text); - } - } - }, - - // Check tweet when it comes it to see if it matches any of our interests. - // If so, the robot will favorite the tweet. - // TODO: Let the robot choose between retweeting or favoriting (or both!) - // TODO: Make sure robot can only favorite a particular tweet once. Might need some sort - // of object or array to properly handle this. - checkInterests: function (tweet) { - if (tweet.id !== null) { - var tweetID; - var tweetUsername; - var tweetText; - - tweetID = tweet.id_str; - tweetUsername = tweet.user.screen_name; - tweetText = tweet.text.toLowerCase(); - - // Check if this particular tweet is a retweet. - // If so, we need to slightly change how data is stored. - if (tweet.retweeted_status) { - //console.log('Favoriting retweet...'); - tweetID = tweet.retweeted_status.id_str; - tweetUsername = tweet.retweeted_status.user.screen_name; - tweetText = tweet.retweeted_status.text.toLowerCase(); - } - - // Check if the tweet coming through is from our own robot. - // If so, abort everything below, because we don't care anymore. - if (config.settings.robotName.toLowerCase() === tweetUsername.toLowerCase()) { - //console.log('Ignoring our own tweet.'); - return; - } - - // Base condition for our robot. Change to true if we've found - // a matching interest. This will prevent us from trying to - // favorite a tweet multiple times. - var foundInterest = false; - var tempInterest; // Store the interest we found within the tweet. - - // Split up the text of the tweet into an array. - var tweetTextArray = tweetText.split(' '); - - // Callback function to check if a particular element is a robot interest. - var isRobotInterest = function(element) { - if (interestsObject[element]) { - tempInterest = element; - foundInterest = true; - } - - return interestsObject[element]; - }; - - // Iterate over the tweetTextArray and see if there's a matching interest - // from the robotInterests object. - tweetTextArray.some(isRobotInterest); - - if (foundInterest) { - var tweetType = 'tweet'; - if (tweet.retweeted_status) tweetType = 'retweet'; - console.log('\n', utils.currentTime(), 'Favoriting the following ' + tweetType + ' from @' + tweetUsername + ' because it mentions \'' + tempInterest + '\':'); - console.log('\n',tweet.text); - - client.post('favorites/create', {id: tweetID}, function(error, tweet, response){ - if(error) { - if (error[0].code === 139) { - // This means we've already favorited this tweet. Ignore it. - } else { - console.log('Error favoriting tweet. Possible API rate limit encountered. Please wait a few moments.'); - console.log(error); - } - } - }); - } - } - }, - - // Check if the user is in our ignore list. If so, we're not going to write a reply. - checkIgnored: function(username) { - if (config.settings.ignoredUsers.indexOf(username.toLowerCase()) != -1) { - return true; - } else { - return false; - } - }, - - - // Check if the Twitter stream is active. It sometimes has a nasty habit of dying. - // Sometimes, Twitter can drop our stream. Based on this document, - // we check out lastTweetTime stamp to see if - // it's been longer than 90 seconds since the last Tweet. - // If so, restart the stream! - // More info: https://dev.twitter.com/streaming/overview/connecting - checkStream: function() { - var curTime = Math.floor(Date.now() / 1000); - // It's been longer than around 120 seconds since we've seen a tweet. Let's restart the stream. - // console.log('[' + utils.currentTime() + '] Debug - lastTweetReceivedTime: ', config.settings.lastTweetReceivedTime); - if (curTime - config.settings.lastTweetReceivedTime >= 120) { - console.log('Stream may have dropped. Restarting stream...'); - this.watchStream(); - } - }, - - // Send a tweet! - postNewTweet: function(send_msg) { - if (config.settings.postTweets) { - client.post('statuses/update', {status: send_msg}, function(error, tweet, response){ - if(error) { - console.log('Error posting tweet. Possible API rate limit encountered. Please wait a few moments'); - console.log(error); - } - }); - } - }, - - // If we're writing a reply to a tweet, let's pass in the username and the ID of the tweet. - writeReply: function(username, replyID, replytext) { - - if (this.checkIgnored(username)) { - console.log("\nUser is on ignore list. Not replying."); - } else { - - // Wrapping everything in a timeout function so that we don't reply instantaneously - var randomDelay = Math.floor((Math.random() * 5) + 1); // Random delay between 1 and 15 seconds - var self = this; - setTimeout(function () { - var replyTweet; - - // We're going to try to reply to the user with self. - var myReply = generator.makeSentenceFromKeyword(replytext); - - if (typeof myReply !== 'undefined') { - console.log('\nGenerating a contextual reply to user.'); - replyTweet = '@' + username + ' ' + myReply; - } else { - console.log('\nGenerating a random reply to user.'); - replyTweet = generator.twitterFriendly(true, username); - } - - console.log('\nReplying to user @' + username + ':'); - console.log(replyTweet); - if (config.settings.respondReplies) self.sendReply(replyTweet,replyID); - }, (randomDelay * 1000)); - } - }, - - // Send a reply. - // TODO: Merge this with the send a tweet function above. - sendReply: function(send_msg, replyID) { - client.post('statuses/update', {status: send_msg, in_reply_to_status_id: replyID}, function(error, tweet, response){ - if(error) { - console.log('Error posting reply. Possible API rate limit encountered. Please wait a few moments'); - //console.log(error); - } - }); - }, - - // Check for any new followers - // If new follower is found, let's be friendly and follow them back! - getFollowers: function() { - if (!config.settings.followUsers) { - console.log("\nWarning: Cannot get users list \n\'follow new users\' is disabled in configuration."); - return; - } - - client.get('followers/list', {count: 3}, function(error, followers, response){ - if(error) { - console.log('followers list error', error); - } - - if(!error) { - var followerArray = []; - var newFollower = followers.users[0].screen_name; - - client.post('friendships/create', {screen_name: newFollower}, function(error, tweet, response){ - if (error) console.log('create friendships error', error); - if (!error) { - if (!tweet.following) { // Check response to see whether or not we were already following user. If not, hey! Awesome! - console.log('Following user @' + newFollower); - //console.log(tweet); - } - } - }); - } - }); - }, +var Tweets = function Tweets(generator, config) { + this.config = config; + this.generator = generator; + // Initialize a new Twitter client using the provided API keys. + this.client = new Twitter({ + consumer_key: this.config.twitter.consumer_key, + consumer_secret: this.config.twitter.consumer_secret, + access_token_key: this.config.twitter.access_token_key, + access_token_secret: this.config.twitter.access_token_secret + }); +}; + + +// Map robot's interests array to an object for constant time lookup. +Tweets.prototype.mapInterests = function() { + this.config.personality.robotInterests.forEach(function(element) { + interestsObject[element] = true; + }); +}, + +// Initialize our Twitter stream and start monitoring +// new tweets that appear in our as they come in. +Tweets.prototype.watchStream = function (mode) { + // Set proper context for 'this' since it will be called inside some functions. + var self = this; + + // Each time we call the watchStream method (usually on bootup), map + // robot interests array from config component into a local object. + this.mapInterests(); + + if (this.config.settings.monitorStream) { + this.client.stream('user', function(stream){ + console.log('Listening to stream...\n\n'); + + // Start streaming data from Twitter. + stream.on('data', function(tweet) { + // Update time of last tweet that we received so we can check if we've dropped the streaming connection. + self.config.settings.lastTweetReceivedTime = Math.floor(Date.now() / 1000); + + // This handles actions that involve a tweet being deleted. If so, we don't want to call the stuff below. + if (tweet.delete || tweet.friends) { + //console.log('Detected deletion request from Twitter API...'); + } else { + // Look at contents of the tweet and determine if we should favorite it. + if (self.config.settings.canFavoriteTweets) { + self.checkInterests(tweet); // Potentially favorite tweet based on interests of our robot. + } + + // Look at Tweet and determine if it's a reply to our robot. + if (tweet.id !== null) { + self.checkReply(tweet); + } + //console.log(tweet); + } + }); + + // Check if we want to destroy the stream + // I don't think this is really working at the moment. + if (mode === 'destroy') { + self.config.settings.monitorStream = false; + //stream.destroy(); + //return; + } + + stream.on('error', function(error) { + console.log('Error detected with streaming API.'); + console.log(error); + }); + }); + } else if (!this.config.settings.monitorStream) { + console.log('\nWarning: Monitoring Twitter stream is currently disabled \ndue to configuration setting.'); + } else { + console.log('\nWarning: Something happened while trying watch the Twitter Stream and I\'m not sure what it is.'); + } +}; + +// Count the number of times we've recently replied to a user. +Tweets.prototype.countReplies = function(user) { + var countReplies = this.config.settings.trackReplies.reduce(function(n, val) { + return n + (val === user); + }, 0); + + //console.log(trackReplies); + + if (countReplies >= 5) { + console.log("Warning: Replies to @" + user + " are Temporarily paused so we don't spam them."); + } + + return countReplies; +}; +// Check if a new tweet in our stream is a reply to us. +Tweets.prototype.checkReply = function(tweet) { + var checkTweet = tweet.text.toLowerCase(); + var myUsername = '@' + this.config.settings.robotName.toLowerCase(); + //var randomReply = false; // Initial condition that makes bot decide whether or not to reply to someone in its feed. + + var replyID = tweet.id_str; // Get the ID of the tweet so we can properly reply to it. + var replyUsername = tweet.user.screen_name; + var replyCount = 0; + + // Randomly reply to tweets that pop up in our stream. + var x = Math.random(); // Generate random number to determine whether we will reply or not + if (this.config.settings.randomReplies && x <= config.settings.randomRepliesChance && checkTweet.indexOf(myUsername) == -1 && (typeof tweet.retweeted_status === "undefined")) { + + //replyCount = this.countReplies(replyUsername); // Get the number of times we've recently replied to this user. + + // Prevent robot from going into a reply loop with itself and imploding the universe! + if (replyUsername.toLowerCase() !== this.config.settings.robotName.toLowerCase() && replyCount < 5) { + this.config.settings.trackReplies.push(replyUsername); // Add user to our reply tracker so we don't spam them too much. + console.log('\nRandomly replying to the following tweet from @' + replyUsername + ':'); + console.log(tweet.text); + + + this.writeReply(replyUsername, replyID, tweet.text); + } + } + + // Check if the tweet is a retweet so we don't gum up our replies with crazy username garbage. + if (typeof tweet.retweeted_status !== "undefined") { + //console.log("\n\n\n!!!!!!!!!!!! ALERT: RETWEET!!!!!\n\n\n"); + } + + // Build a standard reply to a user who mentions us. + if (checkTweet.indexOf(myUsername) != -1 && (typeof tweet.retweeted_status == "undefined")) { + var tempUserArray = []; // Keep track of all the users mentioned in this tweet. + var tempAdditionalUsers; // We'll use this to generate a string of additional users. + + // Checks tweet for any additional mentions and adds them to a temporary array. + var checkMentions = tweet.text.split(' '); + for (var i = 0; i < checkMentions.length; i++) { + if (checkMentions[i].substring(0, 1) == "@") { + // Make sure we aren't adding our own robot to the array + if (checkMentions[i].toLowerCase() != '@' + this.config.settings.robotName.toLowerCase()) { + checkMentions[i] = checkMentions[i].replace(/[\W]*$/g,''); // Remove any cruft at end of username like question marks. + //console.log('Found additional user mentioned: ' + checkMentions[i]); + tempUserArray.push(checkMentions[i]); + } + } + } + + // See if we have any additional users that we'll pass to our reply function below. + var replyUsers; + if (tempUserArray.length > 0) { + tempAdditionalUsers = tempUserArray.join(' '); + replyUsers = replyUsername + ' ' + tempAdditionalUsers; + } else { + replyUsers = replyUsername; + } + + //replyCount = this.countReplies(replyUsername); // Get the number of times we've recently replied to this user. + + // Prevent robot from going into a reply loop with itself! + if (replyUsername.toLowerCase() !== this.config.settings.robotName.toLowerCase() && replyCount < 5) { + // Quick and dirty way to add any bots to our temporary replies blacklist. + if (this.config.personality.otherBots.indexOf(replyUsername.toLowerCase()) != -1) { + console.log('User \'' + replyUsername +'\' is in our bot list. Temporarily limiting replies to this user.'); + //this.config.settings.trackReplies.push(replyUsername,replyUsername,replyUsername,replyUsername,replyUsername,replyUsername); + } + + //this.config.settings.trackReplies.push(replyUsername); // Add user to our reply tracker so we don't spam them too much. + console.log('\nNew reply from @' + replyUsername + ':'); + console.log(tweet.text); + + this.writeReply(replyUsers, replyID, tweet.text); + } + } }; +// Check tweet when it comes it to see if it matches any of our interests. +// If so, the robot will favorite the tweet. +// TODO: Let the robot choose between retweeting or favoriting (or both!) +// TODO: Make sure robot can only favorite a particular tweet once. Might need some sort +// of object or array to properly handle this. +Tweets.prototype.checkInterests = function (tweet) { + if (tweet.id !== null) { + var tweetID; + var tweetUsername; + var tweetText; + + tweetID = tweet.id_str; + tweetUsername = tweet.user.screen_name; + tweetText = tweet.text.toLowerCase(); + + // Check if this particular tweet is a retweet. + // If so, we need to slightly change how data is stored. + if (tweet.retweeted_status) { + //console.log('Favoriting retweet...'); + tweetID = tweet.retweeted_status.id_str; + tweetUsername = tweet.retweeted_status.user.screen_name; + tweetText = tweet.retweeted_status.text.toLowerCase(); + } + + // Check if the tweet coming through is from our own robot. + // If so, abort everything below, because we don't care anymore. + if (this.config.settings.robotName.toLowerCase() === tweetUsername.toLowerCase()) { + //console.log('Ignoring our own tweet.'); + return; + } + + // Base condition for our robot. Change to true if we've found + // a matching interest. This will prevent us from trying to + // favorite a tweet multiple times. + var foundInterest = false; + var tempInterest; // Store the interest we found within the tweet. + + // Split up the text of the tweet into an array. + var tweetTextArray = tweetText.split(' '); + + // Callback function to check if a particular element is a robot interest. + var isRobotInterest = function(element) { + if (interestsObject[element]) { + tempInterest = element; + foundInterest = true; + } + + return interestsObject[element]; + }; + + // Iterate over the tweetTextArray and see if there's a matching interest + // from the robotInterests object. + tweetTextArray.some(isRobotInterest); + + if (foundInterest) { + var tweetType = 'tweet'; + if (tweet.retweeted_status) tweetType = 'retweet'; + console.log('\n', utils.currentTime(), 'Favoriting the following ' + tweetType + ' from @' + tweetUsername + ' because it mentions \'' + tempInterest + '\':'); + console.log('\n',tweet.text); + + this.client.post('favorites/create', {id: tweetID}, function(error, tweet, response){ + if(error) { + if (error[0].code === 139) { + // This means we've already favorited this tweet. Ignore it. + } else { + console.log('Error favoriting tweet. Possible API rate limit encountered. Please wait a few moments.'); + console.log(error); + } + } + }); + } + } +}, + +// Check if the user is in our ignore list. If so, we're not going to write a reply. +Tweets.prototype.checkIgnored = function(username) { + if (this.config.settings.ignoredUsers.indexOf(username.toLowerCase()) != -1) { + return true; + } else { + return false; + } +}, + + +// Check if the Twitter stream is active. It sometimes has a nasty habit of dying. +// Sometimes, Twitter can drop our stream. Based on this document, +// we check out lastTweetTime stamp to see if +// it's been longer than 90 seconds since the last Tweet. +// If so, restart the stream! +// More info: https://dev.twitter.com/streaming/overview/connecting +Tweets.prototype.checkStream = function() { + var curTime = Math.floor(Date.now() / 1000); + // It's been longer than around 120 seconds since we've seen a tweet. Let's restart the stream. + // console.log('[' + utils.currentTime() + '] Debug - lastTweetReceivedTime: ', this.config.settings.lastTweetReceivedTime); + if (curTime - this.config.settings.lastTweetReceivedTime >= 3600) { + console.log('Stream may have dropped. Restarting stream...'); + this.watchStream(); + } +}; + +// Send a tweet! +Tweets.prototype.postNewTweet = function(send_msg) { + if (this.config.settings.postTweets) { + this.client.post('statuses/update', {status: send_msg}, function(error, tweet, response){ + if(error) { + console.log('Error posting tweet. Possible API rate limit encountered. Please wait a few moments'); + console.log(error); + } + }); + } +}; + +// If we're writing a reply to a tweet, let's pass in the username and the ID of the tweet. +Tweets.prototype.writeReply = function(username, replyID, replytext) { + + if (this.checkIgnored(username)) { + console.log("\nUser is on ignore list. Not replying."); + } else { + + // Wrapping everything in a timeout function so that we don't reply instantaneously + var randomDelay = Math.floor((Math.random() * 5) + 1); // Random delay between 1 and 15 seconds + var self = this; + setTimeout(function () { + var replyTweet; + + // We're going to try to reply to the user with self. + var myReply = self.generator.makeSentenceFromKeyword(replytext); + + if (typeof myReply !== 'undefined') { + console.log('\nGenerating a contextual reply to user.'); + replyTweet = '@' + username + ' ' + myReply; + } else { + console.log('\nGenerating a random reply to user.'); + replyTweet = self.generator.twitterFriendly(true, username); + } + + console.log('\nReplying to user @' + username + ':'); + console.log(replyTweet); + if (self.config.settings.respondReplies) self.sendReply(replyTweet,replyID); + }, (randomDelay * 1000)); + } +}, + +// Send a reply. +// TODO: Merge this with the send a tweet function above. +Tweets.prototype.sendReply = function(send_msg, replyID) { + this.client.post('statuses/update', {status: send_msg, in_reply_to_status_id: replyID}, function(error, tweet, response){ + if(error) { + console.log('Error posting reply. Possible API rate limit encountered. Please wait a few moments'); + //console.log(error); + } + }); +}; + +// Check for any new followers +// If new follower is found, let's be friendly and follow them back! +Tweets.prototype.getFollowers = function() { + var self = this; + if (!this.config.settings.followUsers) { + console.log("\nWarning: Cannot get users list \n\'follow new users\' is disabled in configuration."); + return; + } + + this.client.get('followers/list', {count: 3}, function(error, followers, response){ + if(error) { + console.log('followers list error', error); + } + + if(!error) { + var followerArray = []; + var newFollower = followers.users[0].screen_name; + + self.client.post('friendships/create', {screen_name: newFollower}, function(error, tweet, response){ + if (error) console.log('create friendships error', error); + if (!error) { + if (!tweet.following) { // Check response to see whether or not we were already following user. If not, hey! Awesome! + console.log('Following user @' + newFollower); + //console.log(tweet); + } + } + }); + } + }); +}; + + +module.exports = Tweets; ///////// // Object to simulate a fake tweet object so we can check replies and favorites. // var fakeTweet = { @@ -378,4 +382,4 @@ module.exports = { // module.exports.mapInterests(); // module.exports.checkInterests(fakeTweet); -//module.exports.watchStream(); \ No newline at end of file +//module.exports.watchStream(); diff --git a/components/utilities/index.js b/components/utilities/index.js index d3758c4..4147552 100644 --- a/components/utilities/index.js +++ b/components/utilities/index.js @@ -1,24 +1,21 @@ /** * Utilities and Helper Functions - * + * */ -var config = require('../../config'); -var tweet = require('../tweets'); - module.exports = { // Useful for outputting timestamps to the console for actions from our robot. currentTime: function() { // Create a new Javascript Date object based on the timestamp // Based on following solution: http://stackoverflow.com/questions/847185/convert-a-unix-timestamp-to-time-in-javascript var date = new Date(Date.now()); - + // hours part from the timestamp var hours = date.getHours(); - + // minutes part from the timestamp var minutes = "0" + date.getMinutes(); - + // seconds part from the timestamp var seconds = "0" + date.getSeconds(); @@ -31,4 +28,4 @@ module.exports = { return formattedTime; } -}; \ No newline at end of file +}; diff --git a/config/config.example.js b/config/config.js similarity index 76% rename from config/config.example.js rename to config/config.js index 79ef773..43b613e 100644 --- a/config/config.example.js +++ b/config/config.js @@ -1,13 +1,13 @@ /** * CONFIG.JS - * + * * Enter your Twitter API credential here. * You can get Twitter credentials here: * https://dev.twitter.com/ */ module.exports = { - /** + /** * Twitter API Keys * You can get Twitter credentials here: * https://dev.twitter.com/ @@ -16,26 +16,32 @@ module.exports = { consumer_key: '', consumer_secret: '', access_token_key: '', - access_token_secret: '' + access_token_secret: '' }, - /** + /** * General Robot Settings */ settings: { // Your robot's Twitter username (without the @ symbol) - // We use this to search for mentions of the robot + // We use this to search for mentions of the robot // and to prevent it from replying to itself robotName: "MyTwitterRobot", - // Interval for new tweets in seconds. Most users will - // probably want to tweet once every hour or so, + // The absolute path to your robot's source for new + // tweets. Each line will be treated as a separate and + // unique sentence. If none is provided there is a + // small default file that will be used. + corpus: '', + + // Interval for new tweets in seconds. Most users will + // probably want to tweet once every hour or so, // that way the bot isn't too spammy. (1 hour = 3600 seconds); - // Note: Since Javascript timers are in milliseconds, - // we'll multiply by 1,000 in another function. + // Note: Since Javascript timers are in milliseconds, + // we'll multiply by 1,000 in another function. postInterval: 300, - // Tweet on startup? This will compose a tweet the moment + // Tweet on startup? This will compose a tweet the moment // the bot is first run, rather than wait for the full interval. tweetOnStartup: true, @@ -48,27 +54,27 @@ module.exports = { // (and enables everything below). // Default: true watchStream: true, - + // If true, respond to DMs. False prevents it from responding to DMs. // TODO: Fix this. - respondDMs: false, + respondDMs: false, - // If true, have the bot randomly reply to tweets that + // If true, have the bot randomly reply to tweets that // appear in its stream. // Default: false - randomReplies: false, - + randomReplies: false, + // If true, we can repspond to replies! respondReplies: true, - - // If true, let the robot post to Twitter. + + // If true, let the robot post to Twitter. // False prevents it from outputting to Twitter. - postTweets: false, - + postTweets: false, + // If true, allow the bot to favorite tweets // based on the robot's personality settings (see below). getFavs: true, - + // If true, allow bot to follow new users that have followed it. followUsers: true, @@ -78,38 +84,38 @@ module.exports = { ** of times within a certain time frame, let's not reply to them ** anymore for a bit in order to keep everyone's sanity. **/ - trackReplies: [], + trackReplies: [], }, // Configure your robot's interests! personality: { - // A list of things the robot will be interested in and want + // A list of things the robot will be interested in and want // to favorite (and potentially respond to). // These are case insensitive. robotInterests: ['cyborgs','bot','bots','robot','robots','wall-e'], - // These are friends' bots and others that we want to interact with - // but prevent a reply chain loop. Add usernames here without the @ symbol. + // These are friends' bots and others that we want to interact with + // but prevent a reply chain loop. Add usernames here without the @ symbol. // These are case insensitive. otherBots:['roboderp'], - // Hashtags to ignore so our bot doesn't inadvertantly get drawn into + // Hashtags to ignore so our bot doesn't inadvertantly get drawn into // controversial, sensitive, or tragic topics we may have tweeted about in the past. // These are case insensitive ignoredHashtags: ['#RobotsAreEvil'], /* Percent chance that the bot will add additional emojis - ** to the end of a tweet. e.g., .3 = 30%. + ** to the end of a tweet. e.g., .3 = 30%. */ addEmojis: 0.2, /* Percent chance that the bot will add additional hashtags - ** to the end of a tweet. e.g., .2 = 20%. + ** to the end of a tweet. e.g., .2 = 20%. */ addHashtags: 0.1, /* Check whether or not our bot is allowed to randomly reply to other - ** users on its own. + ** users on its own. ** Default: false */ randomReplies: false, @@ -120,4 +126,4 @@ module.exports = { */ randomRepliesChance: 0.05, } -}; \ No newline at end of file +}; diff --git a/config/index.js b/config/index.js index be5eb66..0a31abf 100644 --- a/config/index.js +++ b/config/index.js @@ -1 +1 @@ -module.exports = require('./config.js'); \ No newline at end of file +module.exports = require('./config.js'); diff --git a/package.json b/package.json index 29e8651..d9eaea2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nodeEbot", - "version": "0.2.0", + "version": "0.3.0", "description": "A twitter_ebooks style bot for Node.js ", "main": "bot.js", "scripts": { @@ -15,6 +15,7 @@ "license": "BSD-2-Clause", "dependencies": { "bluebird": "^2.10.0", + "lodash": "^4.11.2", "twitter": "^1.2.5" } }