From e8fab14b6c1c8d4a55803a8bd1a7ac4b50b66424 Mon Sep 17 00:00:00 2001 From: Gigi Date: Fri, 25 Jan 2019 18:36:01 -0600 Subject: [PATCH 01/17] Primitive attempt to get replies via search --- src/bot.js | 5 +++++ test/quotetest.js | 53 +++++++++++++++++++++++++++++++++++++++++++++ test/replytest.js | 20 +++++++++++++++++ test/test.js | 55 ++--------------------------------------------- 4 files changed, 80 insertions(+), 53 deletions(-) create mode 100644 test/quotetest.js create mode 100644 test/replytest.js diff --git a/src/bot.js b/src/bot.js index 2e6fe32..5b695ce 100644 --- a/src/bot.js +++ b/src/bot.js @@ -73,5 +73,10 @@ function postQuote(quote) { } } +function getRepliesAskingForSource(callback) { + bot.get('search/tweets', { q: 'to:@QuotableSatoshi source', count: 100 }, callback) +} + module.exports.shortenQuote = shortenQuote; module.exports.quotes = quotes; +module.exports.getRepliesAskingForSource = getRepliesAskingForSource; diff --git a/test/quotetest.js b/test/quotetest.js new file mode 100644 index 0000000..b3e14f5 --- /dev/null +++ b/test/quotetest.js @@ -0,0 +1,53 @@ +var assert = require('assert'); +var bot = require('../src/bot'); + +const QUOTE_SHORT = "When there are multiple double-spent versions of the same transaction, one and only one will become valid." + +const QUOTE_LONG = "The proof-of-work chain is a solution to the Byzantine Generals' Problem. I'll try to rephrase it in that context.\nA number of Byzantine Generals each have a computer and want to attack the King's wi-fi by brute forcing the password, which they've learned is a certain number of characters in length. Once they stimulate the network to generate a packet, they must crack the password within a limited time to break in and erase the logs, otherwise they will be discovered and get in trouble. They only have enough CPU proof-of-worker to crack it fast enough if a majority of them attack at the same time. \n They don't particularly care when the attack will be, just that they all agree. It has been decided that anyone who feels like it will announce a time, and whatever time is heard first will be the official attack time. The problem is that the network is not instantaneous, and if two generals announce different attack times at close to the same time, some may hear one first and others hear the other first. They use a proof-of-work chain to solve the problem. Once each general receives whatever attack time he hears first, he sets his computer to solve an extremely difficult proof-of-work problem that includes the attack time in its hash. The proof-of-work is so difficult, it's expected to take 10 minutes of them all working at once before one of them finds a solution. Once one of the generals finds a proof-of-work, he broadcasts it to the network, and everyone changes their current proof-of-work computation to include that proof-of-work in the hash they're working on. If anyone was working on a different attack time, they switch to this one, because its proof-of-work chain is now longer.\n After two hours, one attack time should be hashed by a chain of 12 proofs-of-work. Every general, just by verifying the difficulty of the proof-of-work chain, can estimate how much parallel CPU proof-of-worker per hour was expended on it and see that it must have required the majority of the computers to produce that much proof-of-work in the allotted time. They had to all have seen it because the proof-of-work is proof that they worked on it. If the CPU proof-of-worker exhibited by the proof-of-work chain is sufficient to crack the password, they can safely attack at the agreed time.\n The proof-of-work chain is how all the synchronisation, distributed database and global view problems you've asked about are solved." +const QUOTE_LONG_SHORTENED = "The proof-of-work chain is a solution to the Byzantine Generals' Problem. I'll try to rephrase it in that context." + +const TRICKY_QUOTE = "The proof-of-work chain is a solution to the Byzantine Generals' Problem. I'll try to rephrase it in that context.\nA number of Byzantine Generals each have a computer and want to attack the King's wi-fi by brute forcing the password, which they've learned is a certain number of characters in length. Once they stimulate the network to generate a packet, they must crack the password within a limited time to break in and erase the logs, otherwise they will be discovered and get in trouble. They only have enough CPU proof-of-worker to crack it fast enough if a majority of them attack at the same time. \n They don't particularly care when the attack will be, just that they all agree. It has been decided that anyone who feels like it will announce a time, and whatever time is heard first will be the official attack time. The problem is that the network is not instantaneous, and if two generals announce different attack times at close to the same time, some may hear one first and others hear the other first. They use a proof-of-work chain to solve the problem. Once each general receives whatever attack time he hears first, he sets his computer to solve an extremely difficult proof-of-work problem that includes the attack time in its hash. The proof-of-work is so difficult, it's expected to take 10 minutes of them all working at once before one of them finds a solution. Once one of the generals finds a proof-of-work, he broadcasts it to the network, and everyone changes their current proof-of-work computation to include that proof-of-work in the hash they're working on. If anyone was working on a different attack time, they switch to this one, because its proof-of-work chain is now longer.\n After two hours, one attack time should be hashed by a chain of 12 proofs-of-work. Every general, just by verifying the difficulty of the proof-of-work chain, can estimate how much parallel CPU proof-of-worker per hour was expended on it and see that it must have required the majority of the computers to produce that much proof-of-work in the allotted time. They had to all have seen it because the proof-of-work is proof that they worked on it. If the CPU proof-of-worker exhibited by the proof-of-work chain is sufficient to crack the password, they can safely attack at the agreed time.\n The proof-of-work chain is how all the synchronisation, distributed database and global view problems you've asked about are solved." +const TRICKY_QUOTE_SHORTENED = "The proof-of-work chain is a solution to the Byzantine Generals' Problem. I'll try to rephrase it in that context." + +const TRICKY_QUOTE2 = "It is a global distributed database, with additions to the database by consent of the majority, based on a set of rules they follow: \n\n- Whenever someone finds proof-of-work to generate a block, they get some new coins\n- The proof-of-work difficulty is adjusted every two weeks to target an average of 6 blocks per hour (for the whole network)\n- The coins given per block is cut in half every 4 years" +const TRICKY_QUOTE2_SHORTENED = "It is a global distributed database, with additions to the database by consent of the majority, based on a set of rules they follow: \n\n- Whenever someone finds proof-of-work to generate a block, they get some new coins\n- The proof-of-work difficulty is adjusted every two weeks..." + +describe('quotableSatoshi', function() { + describe('#shortenQuote()', function() { + it('should shorten a quote when it is too long', function() { + assert.equal(bot.shortenQuote(QUOTE_LONG), QUOTE_LONG_SHORTENED); + }); + it('should do nothing if a quote is tweetable', function() { + assert.equal(bot.shortenQuote(QUOTE_SHORT), QUOTE_SHORT); + }); + it('should not return an empty string for any quote', function() { + for (i = 0; i < bot.quotes.length; i++) { + var quote = bot.quotes[i].text + var shortened = bot.shortenQuote(quote) + logShortenedQuote(quote, shortened) + assert.notEqual(shortened, ""); + } + }); + it('should shorten this tricky quote successfully', function() { + var quote = TRICKY_QUOTE + var shortened = bot.shortenQuote(quote) + logShortenedQuote(quote, shortened) + assert.equal(shortened, TRICKY_QUOTE_SHORTENED); + }); + it('should shorten this other tricky quote successfully', function() { + var quote = TRICKY_QUOTE2 + var shortened = bot.shortenQuote(quote) + logShortenedQuote(quote, shortened) + assert.equal(shortened, TRICKY_QUOTE2_SHORTENED); + }); + }); +}); + +function logShortenedQuote(quote, shortened) { + console.log(i) + console.log(quote) + console.log("---") + console.log(shortened) + console.log(quote.length + " --> " + shortened.length) + console.log("===================") +} diff --git a/test/replytest.js b/test/replytest.js new file mode 100644 index 0000000..038cc38 --- /dev/null +++ b/test/replytest.js @@ -0,0 +1,20 @@ +var assert = require('assert'); +var bot = require('../src/bot'); + +describe('quotableSatoshi', function() { + describe('#getRepliesAskingForSource()', function() { + it('should get at least one reply', function() { + bot.getRepliesAskingForSource(onSearchComplete) + }); + }); +}); + +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) + } +} diff --git a/test/test.js b/test/test.js index b3e14f5..6bf0c0f 100644 --- a/test/test.js +++ b/test/test.js @@ -1,53 +1,2 @@ -var assert = require('assert'); -var bot = require('../src/bot'); - -const QUOTE_SHORT = "When there are multiple double-spent versions of the same transaction, one and only one will become valid." - -const QUOTE_LONG = "The proof-of-work chain is a solution to the Byzantine Generals' Problem. I'll try to rephrase it in that context.\nA number of Byzantine Generals each have a computer and want to attack the King's wi-fi by brute forcing the password, which they've learned is a certain number of characters in length. Once they stimulate the network to generate a packet, they must crack the password within a limited time to break in and erase the logs, otherwise they will be discovered and get in trouble. They only have enough CPU proof-of-worker to crack it fast enough if a majority of them attack at the same time. \n They don't particularly care when the attack will be, just that they all agree. It has been decided that anyone who feels like it will announce a time, and whatever time is heard first will be the official attack time. The problem is that the network is not instantaneous, and if two generals announce different attack times at close to the same time, some may hear one first and others hear the other first. They use a proof-of-work chain to solve the problem. Once each general receives whatever attack time he hears first, he sets his computer to solve an extremely difficult proof-of-work problem that includes the attack time in its hash. The proof-of-work is so difficult, it's expected to take 10 minutes of them all working at once before one of them finds a solution. Once one of the generals finds a proof-of-work, he broadcasts it to the network, and everyone changes their current proof-of-work computation to include that proof-of-work in the hash they're working on. If anyone was working on a different attack time, they switch to this one, because its proof-of-work chain is now longer.\n After two hours, one attack time should be hashed by a chain of 12 proofs-of-work. Every general, just by verifying the difficulty of the proof-of-work chain, can estimate how much parallel CPU proof-of-worker per hour was expended on it and see that it must have required the majority of the computers to produce that much proof-of-work in the allotted time. They had to all have seen it because the proof-of-work is proof that they worked on it. If the CPU proof-of-worker exhibited by the proof-of-work chain is sufficient to crack the password, they can safely attack at the agreed time.\n The proof-of-work chain is how all the synchronisation, distributed database and global view problems you've asked about are solved." -const QUOTE_LONG_SHORTENED = "The proof-of-work chain is a solution to the Byzantine Generals' Problem. I'll try to rephrase it in that context." - -const TRICKY_QUOTE = "The proof-of-work chain is a solution to the Byzantine Generals' Problem. I'll try to rephrase it in that context.\nA number of Byzantine Generals each have a computer and want to attack the King's wi-fi by brute forcing the password, which they've learned is a certain number of characters in length. Once they stimulate the network to generate a packet, they must crack the password within a limited time to break in and erase the logs, otherwise they will be discovered and get in trouble. They only have enough CPU proof-of-worker to crack it fast enough if a majority of them attack at the same time. \n They don't particularly care when the attack will be, just that they all agree. It has been decided that anyone who feels like it will announce a time, and whatever time is heard first will be the official attack time. The problem is that the network is not instantaneous, and if two generals announce different attack times at close to the same time, some may hear one first and others hear the other first. They use a proof-of-work chain to solve the problem. Once each general receives whatever attack time he hears first, he sets his computer to solve an extremely difficult proof-of-work problem that includes the attack time in its hash. The proof-of-work is so difficult, it's expected to take 10 minutes of them all working at once before one of them finds a solution. Once one of the generals finds a proof-of-work, he broadcasts it to the network, and everyone changes their current proof-of-work computation to include that proof-of-work in the hash they're working on. If anyone was working on a different attack time, they switch to this one, because its proof-of-work chain is now longer.\n After two hours, one attack time should be hashed by a chain of 12 proofs-of-work. Every general, just by verifying the difficulty of the proof-of-work chain, can estimate how much parallel CPU proof-of-worker per hour was expended on it and see that it must have required the majority of the computers to produce that much proof-of-work in the allotted time. They had to all have seen it because the proof-of-work is proof that they worked on it. If the CPU proof-of-worker exhibited by the proof-of-work chain is sufficient to crack the password, they can safely attack at the agreed time.\n The proof-of-work chain is how all the synchronisation, distributed database and global view problems you've asked about are solved." -const TRICKY_QUOTE_SHORTENED = "The proof-of-work chain is a solution to the Byzantine Generals' Problem. I'll try to rephrase it in that context." - -const TRICKY_QUOTE2 = "It is a global distributed database, with additions to the database by consent of the majority, based on a set of rules they follow: \n\n- Whenever someone finds proof-of-work to generate a block, they get some new coins\n- The proof-of-work difficulty is adjusted every two weeks to target an average of 6 blocks per hour (for the whole network)\n- The coins given per block is cut in half every 4 years" -const TRICKY_QUOTE2_SHORTENED = "It is a global distributed database, with additions to the database by consent of the majority, based on a set of rules they follow: \n\n- Whenever someone finds proof-of-work to generate a block, they get some new coins\n- The proof-of-work difficulty is adjusted every two weeks..." - -describe('quotableSatoshi', function() { - describe('#shortenQuote()', function() { - it('should shorten a quote when it is too long', function() { - assert.equal(bot.shortenQuote(QUOTE_LONG), QUOTE_LONG_SHORTENED); - }); - it('should do nothing if a quote is tweetable', function() { - assert.equal(bot.shortenQuote(QUOTE_SHORT), QUOTE_SHORT); - }); - it('should not return an empty string for any quote', function() { - for (i = 0; i < bot.quotes.length; i++) { - var quote = bot.quotes[i].text - var shortened = bot.shortenQuote(quote) - logShortenedQuote(quote, shortened) - assert.notEqual(shortened, ""); - } - }); - it('should shorten this tricky quote successfully', function() { - var quote = TRICKY_QUOTE - var shortened = bot.shortenQuote(quote) - logShortenedQuote(quote, shortened) - assert.equal(shortened, TRICKY_QUOTE_SHORTENED); - }); - it('should shorten this other tricky quote successfully', function() { - var quote = TRICKY_QUOTE2 - var shortened = bot.shortenQuote(quote) - logShortenedQuote(quote, shortened) - assert.equal(shortened, TRICKY_QUOTE2_SHORTENED); - }); - }); -}); - -function logShortenedQuote(quote, shortened) { - console.log(i) - console.log(quote) - console.log("---") - console.log(shortened) - console.log(quote.length + " --> " + shortened.length) - console.log("===================") -} +require('./quotetest.js'); +require('./replytest.js'); From 1e18b575b7a122d01a7a688ce7661c1d3b450315 Mon Sep 17 00:00:00 2001 From: Gigi Date: Sat, 26 Jan 2019 05:38:55 -0600 Subject: [PATCH 02/17] Delete test.js --- test/test.js | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 test/test.js diff --git a/test/test.js b/test/test.js deleted file mode 100644 index 6bf0c0f..0000000 --- a/test/test.js +++ /dev/null @@ -1,2 +0,0 @@ -require('./quotetest.js'); -require('./replytest.js'); From d599328325ac48b89f8a2e7ec82bacf042a7e9ca Mon Sep 17 00:00:00 2001 From: Gigi Date: Sat, 26 Jan 2019 05:40:12 -0600 Subject: [PATCH 03/17] Add function to get parent tweet --- src/bot.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/bot.js b/src/bot.js index 5b695ce..76b7132 100644 --- a/src/bot.js +++ b/src/bot.js @@ -77,6 +77,18 @@ function getRepliesAskingForSource(callback) { bot.get('search/tweets', { q: 'to:@QuotableSatoshi source', count: 100 }, callback) } +function getParentTweet(tweetid, callback) { + bot.get('statuses/show/:id', { id: tweetid }, function(err, data, response) { + if (err) { + callback(err, null, response) + } else { + var parentId = data.in_reply_to_status_id_str + callback(err, parentId, response) + } + }) +} + module.exports.shortenQuote = shortenQuote; module.exports.quotes = quotes; module.exports.getRepliesAskingForSource = getRepliesAskingForSource; +module.exports.getParentTweet = getParentTweet; From 318347f3052126dd9dd6fc2e03a941776d87a6db Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 29 Jan 2019 16:56:39 -0500 Subject: [PATCH 04/17] Add tweet asking for source as test asset --- test/assets/tweet_asking_for_source.json | 101 +++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 test/assets/tweet_asking_for_source.json 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" +} From f18a05a08cc5b5a33256e7769c7d98d77dd9be73 Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 29 Jan 2019 17:38:39 -0500 Subject: [PATCH 05/17] Add source lookup and retrieval of parent tweet --- src/bot.js | 58 +++++++++++++++++++++++++++++++++++++++++++++-- test/replytest.js | 32 +++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/bot.js b/src/bot.js index 76b7132..5475def 100644 --- a/src/bot.js +++ b/src/bot.js @@ -4,6 +4,10 @@ const quotes = require('./quotes.json') const bot = new Twit(config) +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/' + // Pick a random quote var quote = quotes[Math.floor(Math.random()*quotes.length)] @@ -77,13 +81,60 @@ function getRepliesAskingForSource(callback) { bot.get('search/tweets', { q: 'to:@QuotableSatoshi source', count: 100 }, callback) } +function isAwaitingReply(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 replyWithSource() { + // 1. Get list of tweets asking for source + // getRepliesAskingForSource(function(err, data, response) { + // if (err) { + // console.log(err) + // } else { + // data.statuses.forEach(s => { + // 2. Get parent tweet for every tweet asking for source + // console.log(s.text) + // console.log(s.user.screen_name) + // console.log('\n') + // }) + // } + // }) + // 3. Get quote from parent tweet + + // 4. Look up quote source + // 5. Reply to tweet with quote source +} + +function getSourceForQuote(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 source = matched_quotes[0] + switch(source.medium) { + case "whitepaper": + return WHITEPAPER_URL; + case "email": + return EMAIL_URL + source.email_id; + case "bitcointalk": + return BITCOINTALK_URL + source.post_id; + default: + return null; + } +} + function getParentTweet(tweetid, callback) { bot.get('statuses/show/:id', { id: tweetid }, function(err, data, response) { if (err) { callback(err, null, response) } else { - var parentId = data.in_reply_to_status_id_str - callback(err, parentId, response) + var parentId = data.in_reply_to_status_id_str; + if (parentId) { + getParentTweet(parentId, callback); + } } }) } @@ -91,4 +142,7 @@ function getParentTweet(tweetid, callback) { module.exports.shortenQuote = shortenQuote; module.exports.quotes = quotes; module.exports.getRepliesAskingForSource = getRepliesAskingForSource; +module.exports.replyWithSource = replyWithSource; module.exports.getParentTweet = getParentTweet; +module.exports.isAwaitingReply = isAwaitingReply; +module.exports.getSourceForQuote = getSourceForQuote; diff --git a/test/replytest.js b/test/replytest.js index 038cc38..8de93e6 100644 --- a/test/replytest.js +++ b/test/replytest.js @@ -1,10 +1,34 @@ 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' + describe('quotableSatoshi', function() { - describe('#getRepliesAskingForSource()', function() { - it('should get at least one reply', function() { - bot.getRepliesAskingForSource(onSearchComplete) + describe('#getParentTweet()', function() { + it('should get the correct root tweet id of a reply', function() { + bot.getParentTweet(REPLY_TWEET_ID, 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('#isAwaitingReply()', function() { + it('should detect if tweet asking for source was replied to', function() { + bot.isAwaitingReply(TWEET_ASKING_FOR_SOURCE, function(err, data, response) { + assert.equal(data.statuses.length, 0, 'statuses should be empty') + }); + }); + }); + describe('#getSourceForQuote()', function() { + it('should look up the source of a quote correctly', function() { + var source = bot.getSourceForQuote(QUOTE_BITCOINTALK); + console.log(source) + assert.equal(source, QUOTE_BITCOINTALK_SOURCE); }); }); }); @@ -16,5 +40,7 @@ function onSearchComplete(err, data, response) { if (data) { console.log(data) assert.equal(data.count > 0, true) + } else { + fail("No data received") } } From df965811dfaee7b984d9a4652152548d9195f2f4 Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 29 Jan 2019 17:41:06 -0500 Subject: [PATCH 06/17] Add test for whitepaper quote lookup --- test/replytest.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/replytest.js b/test/replytest.js index 8de93e6..ea4a1ea 100644 --- a/test/replytest.js +++ b/test/replytest.js @@ -8,6 +8,9 @@ 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' + describe('quotableSatoshi', function() { describe('#getParentTweet()', function() { it('should get the correct root tweet id of a reply', function() { @@ -25,12 +28,17 @@ describe('quotableSatoshi', function() { }); }); describe('#getSourceForQuote()', function() { - it('should look up the source of a quote correctly', function() { + it('should look up the source of a bitcointalk quote correctly', function() { var source = bot.getSourceForQuote(QUOTE_BITCOINTALK); - console.log(source) assert.equal(source, QUOTE_BITCOINTALK_SOURCE); }); }); + describe('#getSourceForQuote()', function() { + it('should look up the source of a whitepaper quote correctly', function() { + var source = bot.getSourceForQuote(QUOTE_WHITEPAPER); + assert.equal(source, QUOTE_WHITEPAPER_SOURCE); + }); + }); }); function onSearchComplete(err, data, response) { From 44f47ba3979c225d42c68321a20d218f2bcd1174 Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 29 Jan 2019 17:47:13 -0500 Subject: [PATCH 07/17] Add test for email quote lookup --- test/replytest.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/replytest.js b/test/replytest.js index ea4a1ea..8ec90fa 100644 --- a/test/replytest.js +++ b/test/replytest.js @@ -11,6 +11,9 @@ const QUOTE_BITCOINTALK_SOURCE = 'https://satoshi.nakamotoinstitute.org/posts/bi 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' + describe('quotableSatoshi', function() { describe('#getParentTweet()', function() { it('should get the correct root tweet id of a reply', function() { @@ -32,12 +35,14 @@ describe('quotableSatoshi', function() { var source = bot.getSourceForQuote(QUOTE_BITCOINTALK); assert.equal(source, QUOTE_BITCOINTALK_SOURCE); }); - }); - describe('#getSourceForQuote()', function() { it('should look up the source of a whitepaper quote correctly', function() { var source = bot.getSourceForQuote(QUOTE_WHITEPAPER); assert.equal(source, QUOTE_WHITEPAPER_SOURCE); }); + it('should look up the source of a email quote correctly', function() { + var source = bot.getSourceForQuote(QUOTE_EMAIL); + assert.equal(source, QUOTE_EMAIL_SOURCE); + }); }); }); From 0e7085d206ab9627bb541de4d288783205df30fd Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 29 Jan 2019 17:50:33 -0500 Subject: [PATCH 08/17] Add p2pfoundation lookup; add test --- src/bot.js | 3 +++ test/replytest.js | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/src/bot.js b/src/bot.js index 5475def..2046903 100644 --- a/src/bot.js +++ b/src/bot.js @@ -7,6 +7,7 @@ const bot = new Twit(config) 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/' // Pick a random quote var quote = quotes[Math.floor(Math.random()*quotes.length)] @@ -121,6 +122,8 @@ function getSourceForQuote(quote, callback) { return EMAIL_URL + source.email_id; case "bitcointalk": return BITCOINTALK_URL + source.post_id; + case "p2pfoundation": + return P2PFOUNDATION_URL + source.post_id; default: return null; } diff --git a/test/replytest.js b/test/replytest.js index 8ec90fa..f7e5418 100644 --- a/test/replytest.js +++ b/test/replytest.js @@ -14,6 +14,9 @@ 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() { @@ -43,6 +46,10 @@ describe('quotableSatoshi', function() { var source = bot.getSourceForQuote(QUOTE_EMAIL); assert.equal(source, QUOTE_EMAIL_SOURCE); }); + it('should look up the source of a p2pfoundation quote correctly', function() { + var source = bot.getSourceForQuote(QUOTE_P2PFOUNDATION); + assert.equal(source, QUOTE_P2PFOUNDATION_SOURCE); + }); }); }); From 3a2141cc8b0715c3b2d07aef06af334e016695ed Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 29 Jan 2019 20:32:27 -0500 Subject: [PATCH 09/17] Get quote metadata; add source url to metadata --- src/bot.js | 87 ++++++++++++++++++++++++++++++----------------- test/replytest.js | 32 +++++++++++------ 2 files changed, 77 insertions(+), 42 deletions(-) diff --git a/src/bot.js b/src/bot.js index 2046903..1daee57 100644 --- a/src/bot.js +++ b/src/bot.js @@ -87,59 +87,82 @@ function isAwaitingReply(tweet, callback) { bot.get('search/tweets', { q: 'from:@QuotableSatoshi to:' + tweet.user.screen_name, since_id: since_id, count: 10 }, callback) } -function replyWithSource() { +function replyAllWithSource() { // 1. Get list of tweets asking for source - // getRepliesAskingForSource(function(err, data, response) { - // if (err) { - // console.log(err) - // } else { - // data.statuses.forEach(s => { + getRepliesAskingForSource(function(err, data, response) { + if (err) { + console.log(err) + } else { + data.statuses.forEach(s => { // 2. Get parent tweet for every tweet asking for source - // console.log(s.text) - // console.log(s.user.screen_name) - // console.log('\n') - // }) - // } - // }) + console.log(s.text) + console.log(s.user.screen_name) + }) + } + }) // 3. Get quote from parent tweet // 4. Look up quote source // 5. Reply to tweet with quote source } -function getSourceForQuote(quote, callback) { +function replyWithSource(tweet, callback) { + console.log("Getting parent tweet for " + tweet.text) + getParentTweet(tweet, function(err, data, response) { + console.log("Got parent tweet: ") + console.log(data) + var source = getQuoteMetadata(data.text) + + var reply = '@' + reply += tweet.user.screen_name + reply += ' Satoshi wrote this on ' + // tweet.user.screen_name + + if (config.post_to_twitter) { + bot.post('statuses/update', { status: quote_source, in_reply_to_status_id: tweet.id_str }, function(err, data, response) { + console.log(data) + }) + } else { + console.log(source) + console.log("(Not posting quote to timeline. ENV var POST_TO_TWITTER has to be set to true.)") + } + }) +} + +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 source = matched_quotes[0] - switch(source.medium) { + var quote_entry = matched_quotes[0] + var source = null + switch(quote_entry.medium) { case "whitepaper": - return WHITEPAPER_URL; + source = WHITEPAPER_URL; + break; case "email": - return EMAIL_URL + source.email_id; + source = EMAIL_URL + quote_entry.email_id; + break; case "bitcointalk": - return BITCOINTALK_URL + source.post_id; + source = BITCOINTALK_URL + quote_entry.post_id; + break; case "p2pfoundation": - return P2PFOUNDATION_URL + source.post_id; + source = P2PFOUNDATION_URL + quote_entry.post_id; + break; default: - return null; + source = null; + break; } + + quote_entry['source'] = source; + + return quote_entry } -function getParentTweet(tweetid, callback) { - bot.get('statuses/show/:id', { id: tweetid }, function(err, data, response) { - if (err) { - callback(err, null, response) - } else { - var parentId = data.in_reply_to_status_id_str; - if (parentId) { - getParentTweet(parentId, callback); - } - } - }) +function getParentTweet(tweet, callback) { + bot.get('statuses/show/:id', { id: tweet.in_reply_to_status_id_str }, callback) } module.exports.shortenQuote = shortenQuote; @@ -148,4 +171,4 @@ module.exports.getRepliesAskingForSource = getRepliesAskingForSource; module.exports.replyWithSource = replyWithSource; module.exports.getParentTweet = getParentTweet; module.exports.isAwaitingReply = isAwaitingReply; -module.exports.getSourceForQuote = getSourceForQuote; +module.exports.getQuoteMetadata = getQuoteMetadata; diff --git a/test/replytest.js b/test/replytest.js index f7e5418..9ba94f1 100644 --- a/test/replytest.js +++ b/test/replytest.js @@ -20,7 +20,7 @@ const QUOTE_P2PFOUNDATION_SOURCE = 'https://satoshi.nakamotoinstitute.org/posts/ describe('quotableSatoshi', function() { describe('#getParentTweet()', function() { it('should get the correct root tweet id of a reply', function() { - bot.getParentTweet(REPLY_TWEET_ID, function(err, data, response) { + 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') }) @@ -33,22 +33,34 @@ describe('quotableSatoshi', function() { }); }); }); - describe('#getSourceForQuote()', function() { + describe('#getQuoteMetadata()', function() { it('should look up the source of a bitcointalk quote correctly', function() { - var source = bot.getSourceForQuote(QUOTE_BITCOINTALK); - assert.equal(source, QUOTE_BITCOINTALK_SOURCE); + 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 source = bot.getSourceForQuote(QUOTE_WHITEPAPER); - assert.equal(source, QUOTE_WHITEPAPER_SOURCE); + 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 source = bot.getSourceForQuote(QUOTE_EMAIL); - assert.equal(source, QUOTE_EMAIL_SOURCE); + 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 source = bot.getSourceForQuote(QUOTE_P2PFOUNDATION); - assert.equal(source, QUOTE_P2PFOUNDATION_SOURCE); + var metadata = bot.getQuoteMetadata(QUOTE_P2PFOUNDATION); + assert.equal(metadata.source, QUOTE_P2PFOUNDATION_SOURCE); + assert.equal(metadata.date, "February 18, 2009"); + }); + }); + describe('#replyWithSource()', function() { + it('should reply to a tweet asking for source', function() { + bot.replyWithSource(TWEET_ASKING_FOR_SOURCE, function(err, data, response) { + console.log(err) + console.log(data) + }); }); }); }); From 6dedacf7c6a3f7301ff03398ec596b8d135d821c Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 29 Jan 2019 21:33:37 -0500 Subject: [PATCH 10/17] Refactor to getRepliesByBot; add replyAll function --- src/bot.js | 52 +++++++++++++++++++++++++++++++++++------------ test/replytest.js | 4 ++-- 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/src/bot.js b/src/bot.js index 1daee57..740e0e7 100644 --- a/src/bot.js +++ b/src/bot.js @@ -20,6 +20,7 @@ var tweetableQuote = shortenQuote(sanitizedQuote) // Post quote to twitter postQuote(tweetableQuote) +replyAllWithSource() /** * Get rid of Satoshi's double spaces since they use up valuable @@ -82,7 +83,7 @@ function getRepliesAskingForSource(callback) { bot.get('search/tweets', { q: 'to:@QuotableSatoshi source', count: 100 }, callback) } -function isAwaitingReply(tweet, 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) } @@ -94,9 +95,14 @@ function replyAllWithSource() { console.log(err) } else { data.statuses.forEach(s => { - // 2. Get parent tweet for every tweet asking for source - console.log(s.text) - console.log(s.user.screen_name) + // 2. Get parent tweet for every tweet asking for source + getRepliesByBot(s, function(err, data, response) { + if (data.statuses.length == 0) { + replyWithSource(s, function(err, data, response) { + // asdf + }); + } + }); }) } }) @@ -107,28 +113,48 @@ function replyAllWithSource() { } function replyWithSource(tweet, callback) { - console.log("Getting parent tweet for " + tweet.text) getParentTweet(tweet, function(err, data, response) { - console.log("Got parent tweet: ") - console.log(data) - var source = getQuoteMetadata(data.text) - + 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 ' - // tweet.user.screen_name + 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: quote_source, in_reply_to_status_id: tweet.id_str }, function(err, data, response) { console.log(data) }) } else { - console.log(source) - console.log("(Not posting quote to timeline. ENV var POST_TO_TWITTER has to be set to true.)") + 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); }); @@ -170,5 +196,5 @@ module.exports.quotes = quotes; module.exports.getRepliesAskingForSource = getRepliesAskingForSource; module.exports.replyWithSource = replyWithSource; module.exports.getParentTweet = getParentTweet; -module.exports.isAwaitingReply = isAwaitingReply; +module.exports.getRepliesByBot = getRepliesByBot; module.exports.getQuoteMetadata = getQuoteMetadata; diff --git a/test/replytest.js b/test/replytest.js index 9ba94f1..9700ede 100644 --- a/test/replytest.js +++ b/test/replytest.js @@ -26,9 +26,9 @@ describe('quotableSatoshi', function() { }) }); }); - describe('#isAwaitingReply()', function() { + describe('#getRepliesByBot()', function() { it('should detect if tweet asking for source was replied to', function() { - bot.isAwaitingReply(TWEET_ASKING_FOR_SOURCE, function(err, data, response) { + bot.getRepliesByBot(TWEET_ASKING_FOR_SOURCE, function(err, data, response) { assert.equal(data.statuses.length, 0, 'statuses should be empty') }); }); From 06cac7ce20a80a4ad83166cd0b00397a6b36a482 Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 29 Jan 2019 21:57:08 -0500 Subject: [PATCH 11/17] Fix posting of replies --- src/bot.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/bot.js b/src/bot.js index 740e0e7..58e349b 100644 --- a/src/bot.js +++ b/src/bot.js @@ -129,8 +129,12 @@ function replyWithSource(tweet, callback) { console.log(reply) if (config.post_to_twitter) { - bot.post('statuses/update', { status: quote_source, in_reply_to_status_id: tweet.id_str }, function(err, data, response) { - console.log(data) + 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.)") From 30b55f9ae4b7f33f65287ada9c955288db2d3a67 Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 29 Jan 2019 21:57:21 -0500 Subject: [PATCH 12/17] Remove comments; add TODO --- src/bot.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/bot.js b/src/bot.js index 58e349b..019d865 100644 --- a/src/bot.js +++ b/src/bot.js @@ -99,17 +99,13 @@ function replyAllWithSource() { getRepliesByBot(s, function(err, data, response) { if (data.statuses.length == 0) { replyWithSource(s, function(err, data, response) { - // asdf + // TODO catch error }); } }); }) } }) - // 3. Get quote from parent tweet - - // 4. Look up quote source - // 5. Reply to tweet with quote source } function replyWithSource(tweet, callback) { From 8724f67bad5990bdac427c6a894ea14288aac09d Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 29 Jan 2019 22:11:27 -0500 Subject: [PATCH 13/17] Move posting to postRandomQuote --- index.js | 3 ++- src/bot.js | 30 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index 3e3c714..023d8c5 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,4 @@ 'use strict'; -require('./src/bot'); +var bot = require('./src/bot'); +bot.postRandomQuote(); diff --git a/src/bot.js b/src/bot.js index 019d865..4fe6cf8 100644 --- a/src/bot.js +++ b/src/bot.js @@ -9,18 +9,20 @@ 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/' -// Pick a random quote -var quote = quotes[Math.floor(Math.random()*quotes.length)] -// Sanitze quote (remove double spaces) -var sanitizedQuote = sanitizeQuote(quote) +function postRandomQuote() { + // Pick a random quote + var quote = quotes[Math.floor(Math.random()*quotes.length)] -// Reduce length of quote to fit twitter -var tweetableQuote = shortenQuote(sanitizedQuote) + // Sanitze quote (remove double spaces) + var sanitizedQuote = sanitizeQuote(quote) -// Post quote to twitter -postQuote(tweetableQuote) -replyAllWithSource() + // 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 @@ -89,18 +91,14 @@ function getRepliesByBot(tweet, callback) { } function replyAllWithSource() { - // 1. Get list of tweets asking for source getRepliesAskingForSource(function(err, data, response) { if (err) { console.log(err) } else { data.statuses.forEach(s => { - // 2. Get parent tweet for every tweet asking for source getRepliesByBot(s, function(err, data, response) { if (data.statuses.length == 0) { - replyWithSource(s, function(err, data, response) { - // TODO catch error - }); + replyWithSource(s); } }); }) @@ -108,7 +106,7 @@ function replyAllWithSource() { }) } -function replyWithSource(tweet, callback) { +function replyWithSource(tweet) { getParentTweet(tweet, function(err, data, response) { console.log("--") console.log(tweet.text) @@ -195,6 +193,8 @@ 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; From 40ae1870d201a252d53856d3fb49b9e27d44dce6 Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 29 Jan 2019 22:11:56 -0500 Subject: [PATCH 14/17] Remove empty reply test --- test/replytest.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/replytest.js b/test/replytest.js index 9700ede..9364749 100644 --- a/test/replytest.js +++ b/test/replytest.js @@ -55,14 +55,6 @@ describe('quotableSatoshi', function() { assert.equal(metadata.date, "February 18, 2009"); }); }); - describe('#replyWithSource()', function() { - it('should reply to a tweet asking for source', function() { - bot.replyWithSource(TWEET_ASKING_FOR_SOURCE, function(err, data, response) { - console.log(err) - console.log(data) - }); - }); - }); }); function onSearchComplete(err, data, response) { From ff6d01e5cc3c8d5491c6a82e087b749569cde700 Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 29 Jan 2019 22:12:34 -0500 Subject: [PATCH 15/17] Add reply.js --- package.json | 3 ++- reply.js | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 reply.js 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(); From 5a02fa666e2327cf9580d9847aee9fc8d230ba37 Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 29 Jan 2019 22:21:07 -0500 Subject: [PATCH 16/17] Add missing comma in reply --- src/bot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bot.js b/src/bot.js index 4fe6cf8..16e4c78 100644 --- a/src/bot.js +++ b/src/bot.js @@ -115,7 +115,7 @@ function replyWithSource(tweet) { reply += tweet.user.screen_name reply += ' Satoshi wrote this on ' reply += metadata.date - reply += ' ' + reply += ', ' reply += mediumPhrase(metadata.medium) reply += '. You can find the full quote, context, and more information here: ' reply += metadata.source From ed988ccf92e8980fab6afe02f0959351772e2261 Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 29 Jan 2019 22:27:21 -0500 Subject: [PATCH 17/17] Add newline after var --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index 023d8c5..878f288 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,5 @@ 'use strict'; var bot = require('./src/bot'); + bot.postRandomQuote();