diff --git a/index.js b/index.js index 3e3c714..878f288 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,5 @@ 'use strict'; -require('./src/bot'); +var bot = require('./src/bot'); + +bot.postRandomQuote(); diff --git a/package.json b/package.json index 46e3cd0..971a475 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "start": "node index.js", "test": "./node_modules/.bin/mocha --reporter spec", - "lint": "node_modules/.bin/goodparts *.js src/**" + "lint": "node_modules/.bin/goodparts *.js src/**", + "reply": "node reply.js" }, "keywords": [ "twitter", diff --git a/reply.js b/reply.js new file mode 100644 index 0000000..ea24736 --- /dev/null +++ b/reply.js @@ -0,0 +1,4 @@ +'use strict'; + +var bot = require('./src/bot'); +bot.replyAllWithSource(); diff --git a/src/bot.js b/src/bot.js index 2e6fe32..16e4c78 100644 --- a/src/bot.js +++ b/src/bot.js @@ -4,17 +4,25 @@ const quotes = require('./quotes.json') const bot = new Twit(config) -// Pick a random quote -var quote = quotes[Math.floor(Math.random()*quotes.length)] +const WHITEPAPER_URL = 'https://bitcoin.org/bitcoin.pdf' +const BITCOINTALK_URL = 'https://satoshi.nakamotoinstitute.org/posts/bitcointalk/' +const EMAIL_URL = 'https://satoshi.nakamotoinstitute.org/emails/cryptography/' +const P2PFOUNDATION_URL = 'https://satoshi.nakamotoinstitute.org/posts/p2pfoundation/' -// Sanitze quote (remove double spaces) -var sanitizedQuote = sanitizeQuote(quote) -// Reduce length of quote to fit twitter -var tweetableQuote = shortenQuote(sanitizedQuote) +function postRandomQuote() { + // Pick a random quote + var quote = quotes[Math.floor(Math.random()*quotes.length)] -// Post quote to twitter -postQuote(tweetableQuote) + // Sanitze quote (remove double spaces) + var sanitizedQuote = sanitizeQuote(quote) + + // Reduce length of quote to fit twitter + var tweetableQuote = shortenQuote(sanitizedQuote) + + // Post quote to twitter + postQuote(tweetableQuote) +} /** * Get rid of Satoshi's double spaces since they use up valuable @@ -73,5 +81,120 @@ function postQuote(quote) { } } +function getRepliesAskingForSource(callback) { + bot.get('search/tweets', { q: 'to:@QuotableSatoshi source', count: 100 }, callback) +} + +function getRepliesByBot(tweet, callback) { + var since_id = tweet.id_str + bot.get('search/tweets', { q: 'from:@QuotableSatoshi to:' + tweet.user.screen_name, since_id: since_id, count: 10 }, callback) +} + +function replyAllWithSource() { + getRepliesAskingForSource(function(err, data, response) { + if (err) { + console.log(err) + } else { + data.statuses.forEach(s => { + getRepliesByBot(s, function(err, data, response) { + if (data.statuses.length == 0) { + replyWithSource(s); + } + }); + }) + } + }) +} + +function replyWithSource(tweet) { + getParentTweet(tweet, function(err, data, response) { + console.log("--") + console.log(tweet.text) + var metadata = getQuoteMetadata(data.text.substring(0, 70)) // twitter API might return truncated text + var reply = '@' + reply += tweet.user.screen_name + reply += ' Satoshi wrote this on ' + reply += metadata.date + reply += ', ' + reply += mediumPhrase(metadata.medium) + reply += '. You can find the full quote, context, and more information here: ' + reply += metadata.source + + console.log(reply) + + if (config.post_to_twitter) { + bot.post('statuses/update', { status: reply, in_reply_to_status_id: tweet.id_str }, function(err, data, response) { + if (err) { + console.log(err) + } else { + console.log(data) + } + }) + } else { + console.log("(Not replying to user. ENV var POST_TO_TWITTER has to be set to true.)") + } + }) +} + +function mediumPhrase(medium) { + switch(medium) { + case "whitepaper": + return "in the whitepaper" + case "email": + return "in an email to the cryptography mailing list" + case "bitcointalk": + return "in a BitcoinTalk thread" + break; + case "p2pfoundation": + return "in an email to the P2P Foundation mailing list" + default: + source = null; + break; + } +} + +function getQuoteMetadata(quote, callback) { + var PATTERN = new RegExp(quote); + var matched_quotes = quotes.filter(function (q) { return PATTERN.test(q.text); }); + if (!matched_quotes || matched_quotes.length > 1) { + return null; + } + + var quote_entry = matched_quotes[0] + var source = null + switch(quote_entry.medium) { + case "whitepaper": + source = WHITEPAPER_URL; + break; + case "email": + source = EMAIL_URL + quote_entry.email_id; + break; + case "bitcointalk": + source = BITCOINTALK_URL + quote_entry.post_id; + break; + case "p2pfoundation": + source = P2PFOUNDATION_URL + quote_entry.post_id; + break; + default: + source = null; + break; + } + + quote_entry['source'] = source; + + return quote_entry +} + +function getParentTweet(tweet, callback) { + bot.get('statuses/show/:id', { id: tweet.in_reply_to_status_id_str }, callback) +} + module.exports.shortenQuote = shortenQuote; module.exports.quotes = quotes; +module.exports.getRepliesAskingForSource = getRepliesAskingForSource; +module.exports.replyWithSource = replyWithSource; +module.exports.replyAllWithSource = replyAllWithSource; +module.exports.getParentTweet = getParentTweet; +module.exports.getRepliesByBot = getRepliesByBot; +module.exports.getQuoteMetadata = getQuoteMetadata; +module.exports.postRandomQuote = postRandomQuote; diff --git a/test/assets/tweet_asking_for_source.json b/test/assets/tweet_asking_for_source.json new file mode 100644 index 0000000..0b83007 --- /dev/null +++ b/test/assets/tweet_asking_for_source.json @@ -0,0 +1,101 @@ +{ + "created_at": "Fri Jan 25 23:59:44 +0000 2019", + "id": 1088949574264414200, + "id_str": "1088949574264414213", + "text": "@QuotableSatoshi Source?", + "truncated": false, + "entities": { + "hashtags": [], + "symbols": [], + "user_mentions": [ + { + "screen_name": "QuotableSatoshi", + "name": "Quotable Satoshi", + "id": 1081377398514425900, + "id_str": "1081377398514425856", + "indices": [ + 0, + 16 + ] + } + ], + "urls": [] + }, + "source": "Twitter for Android", + "in_reply_to_status_id": 1085371334950092800, + "in_reply_to_status_id_str": "1085371334950092800", + "in_reply_to_user_id": 1081377398514425900, + "in_reply_to_user_id_str": "1081377398514425856", + "in_reply_to_screen_name": "QuotableSatoshi", + "user": { + "id": 18213426, + "id_str": "18213426", + "name": "Gigi", + "screen_name": "dergigi", + "location": "The Internet, Planet Earth", + "description": "Bitcoin twitter is real life.", + "url": "https://t.co/vrNoQgEBxT", + "entities": { + "url": { + "urls": [ + { + "url": "https://t.co/vrNoQgEBxT", + "expanded_url": "https://medium.com/@dergigi", + "display_url": "medium.com/@dergigi", + "indices": [ + 0, + 23 + ] + } + ] + }, + "description": { + "urls": [] + } + }, + "protected": false, + "followers_count": 580, + "friends_count": 162, + "listed_count": 46, + "created_at": "Thu Dec 18 11:49:14 +0000 2008", + "favourites_count": 12596, + "utc_offset": null, + "time_zone": null, + "geo_enabled": false, + "verified": false, + "statuses_count": 4665, + "lang": "en", + "contributors_enabled": false, + "is_translator": false, + "is_translation_enabled": false, + "profile_background_color": "000000", + "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_tile": false, + "profile_image_url": "http://pbs.twimg.com/profile_images/975173422119612417/h2VyxVmR_normal.jpg", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/975173422119612417/h2VyxVmR_normal.jpg", + "profile_banner_url": "https://pbs.twimg.com/profile_banners/18213426/1529070904", + "profile_link_color": "ABB8C2", + "profile_sidebar_border_color": "000000", + "profile_sidebar_fill_color": "000000", + "profile_text_color": "000000", + "profile_use_background_image": false, + "has_extended_profile": false, + "default_profile": false, + "default_profile_image": false, + "following": true, + "follow_request_sent": false, + "notifications": false, + "translator_type": "none" + }, + "geo": null, + "coordinates": null, + "place": null, + "contributors": null, + "is_quote_status": false, + "retweet_count": 0, + "favorite_count": 0, + "favorited": false, + "retweeted": false, + "lang": "en" +} diff --git a/test/test.js b/test/quotetest.js similarity index 100% rename from test/test.js rename to test/quotetest.js diff --git a/test/replytest.js b/test/replytest.js new file mode 100644 index 0000000..9364749 --- /dev/null +++ b/test/replytest.js @@ -0,0 +1,70 @@ +var assert = require('assert'); +var bot = require('../src/bot'); + +const REPLY_TWEET_ID = '1088949574264414213' +const EXPECTED_ROOT_TWEET_ID = '1085371334950092800' +const TWEET_ASKING_FOR_SOURCE = require('./assets/tweet_asking_for_source.json') + +const QUOTE_BITCOINTALK = 'At equilibrium size, many nodes will be server farms with one or two network nodes that feed the rest of the farm over a LAN.' +const QUOTE_BITCOINTALK_SOURCE = 'https://satoshi.nakamotoinstitute.org/posts/bitcointalk/188' + +const QUOTE_WHITEPAPER = 'A purely peer-to-peer version of electronic cash would allow online payments to be sent directly from one party to another without going through a financial institution.' +const QUOTE_WHITEPAPER_SOURCE = 'https://bitcoin.org/bitcoin.pdf' + +const QUOTE_EMAIL = 'It might make sense just to get some in case it catches on. If enough people think the same way, that becomes a self fulfilling prophecy.' +const QUOTE_EMAIL_SOURCE = 'https://satoshi.nakamotoinstitute.org/emails/cryptography/17' + +const QUOTE_P2PFOUNDATION = 'To Sepp\'s question, indeed there is nobody to act as central bank or federal reserve to adjust the money supply as the population of users grows.' +const QUOTE_P2PFOUNDATION_SOURCE = 'https://satoshi.nakamotoinstitute.org/posts/p2pfoundation/3' + +describe('quotableSatoshi', function() { + describe('#getParentTweet()', function() { + it('should get the correct root tweet id of a reply', function() { + bot.getParentTweet(TWEET_ASKING_FOR_SOURCE, function(err, data, response) { + assert.equal(data.id_str, EXPECTED_ROOT_TWEET_ID, 'tweet id is the expected root tweet id') + assert.equal(data.user.screen_name, 'dergigi') + }) + }); + }); + describe('#getRepliesByBot()', function() { + it('should detect if tweet asking for source was replied to', function() { + bot.getRepliesByBot(TWEET_ASKING_FOR_SOURCE, function(err, data, response) { + assert.equal(data.statuses.length, 0, 'statuses should be empty') + }); + }); + }); + describe('#getQuoteMetadata()', function() { + it('should look up the source of a bitcointalk quote correctly', function() { + var metadata = bot.getQuoteMetadata(QUOTE_BITCOINTALK); + assert.equal(metadata.source, QUOTE_BITCOINTALK_SOURCE); + assert.equal(metadata.date, "July 14, 2010"); + }); + it('should look up the source of a whitepaper quote correctly', function() { + var metadata = bot.getQuoteMetadata(QUOTE_WHITEPAPER); + assert.equal(metadata.source, QUOTE_WHITEPAPER_SOURCE); + assert.equal(metadata.date, "October 31, 2008"); + }); + it('should look up the source of a email quote correctly', function() { + var metadata = bot.getQuoteMetadata(QUOTE_EMAIL); + assert.equal(metadata.source, QUOTE_EMAIL_SOURCE); + assert.equal(metadata.date, "January 17, 2009"); + }); + it('should look up the source of a p2pfoundation quote correctly', function() { + var metadata = bot.getQuoteMetadata(QUOTE_P2PFOUNDATION); + assert.equal(metadata.source, QUOTE_P2PFOUNDATION_SOURCE); + assert.equal(metadata.date, "February 18, 2009"); + }); + }); +}); + +function onSearchComplete(err, data, response) { + if (err) { + fail("Search should not throw an error") + } + if (data) { + console.log(data) + assert.equal(data.count > 0, true) + } else { + fail("No data received") + } +}