├── .gitignore ├── 1_node_js ├── index.js └── package.json ├── 2_node_js_aws_lambda ├── index.js └── package.json ├── 3_python_27_aws_lambda └── index.py ├── 4_purescript ├── bower.json ├── package.json ├── src │ ├── Main.purs │ ├── Twitter.js │ ├── Twitter.purs │ └── Twitter │ │ ├── Retweet.js │ │ ├── Retweet.purs │ │ ├── Search.js │ │ ├── Search.purs │ │ ├── Streaming.js │ │ └── Streaming.purs └── test │ └── Main.purs ├── 5_purescript_aws_lambda ├── bower.json ├── index.js ├── package.json ├── src │ ├── AWS │ │ └── Lambda │ │ │ ├── Context.js │ │ │ └── Context.purs │ ├── TweetBot.purs │ ├── Twitter.js │ ├── Twitter.purs │ └── Twitter │ │ ├── Retweet.js │ │ ├── Retweet.purs │ │ ├── Search.js │ │ ├── Search.purs │ │ ├── Streaming.js │ │ └── Streaming.purs ├── test │ └── Main.purs └── twitter.js ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | bower_components 29 | 30 | # credentials 31 | credentials.js 32 | credentials.json 33 | MyCredentials.purs 34 | 35 | # output 36 | output/ 37 | 38 | # python libraries 39 | oauthlib 40 | requests 41 | requests_oauthlib 42 | twython 43 | *.dist-info 44 | -------------------------------------------------------------------------------- /1_node_js/index.js: -------------------------------------------------------------------------------- 1 | var Twitter = require('twitter'); 2 | 3 | /** 4 | * credentials.js should just look like 5 | * 6 | * module.exports = { 7 | * consumer_key: "...", 8 | * consumer_secret: "...", 9 | * access_token_key: "...", 10 | * access_token_secret: "..." 11 | * }; 12 | * 13 | * / 14 | 15 | var credentials = require('./credentials'); 16 | 17 | var client = new Twitter(credentials); 18 | 19 | var query = 'make "great again" -america -filter:retweets'; 20 | var rgx = /make .* great again/i; 21 | 22 | // Runs a Twitter search for the specified `query` and retweets all the results. 23 | function searchAndTweet(succeed, fail) { 24 | console.log("search and tweet"); 25 | client.get('search/tweets', {q: query, count: 15}, function(err, tweets, response) { 26 | if (!tweets.statuses) { 27 | fail(err); 28 | } 29 | 30 | console.log(new Date()); 31 | console.log('found ' + tweets.statuses.length + ' tweets'); 32 | 33 | tweets.statuses.forEach(function(tweet) { 34 | // Make sure we match the regex. 35 | var match = tweet.text.match(rgx); 36 | if (match) { 37 | var tweetId = tweet.id_str; 38 | client.post('statuses/retweet/' + tweetId, function(err, tweet, id) { 39 | // Will return an error if we try to retweet a tweet that we've already 40 | // retweeted. 41 | console.log(err || tweet.text); 42 | }); 43 | } else { 44 | // consider doing something for no match 45 | } 46 | }); 47 | succeed("success"); 48 | }); 49 | } 50 | 51 | setInterval(function() { 52 | searchAndTweet(console.log, console.log); 53 | }, 5 * 60 * 1000); 54 | -------------------------------------------------------------------------------- /1_node_js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-plain", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "BSD-2-Clause", 11 | "dependencies": { 12 | "twitter": "~1.2.5" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /2_node_js_aws_lambda/index.js: -------------------------------------------------------------------------------- 1 | var Twitter = require('twitter'); 2 | 3 | /** 4 | * credentials.js should look like 5 | * 6 | * module.exports = { 7 | * consumer_key: "...", 8 | * consumer_secret: "...", 9 | * access_token_key: "...", 10 | * access_token_secret: "..." 11 | * }; 12 | */ 13 | 14 | var credentials = require('./credentials'); 15 | var client = new Twitter(credentials); 16 | 17 | /** 18 | * We want to find tweets that look like "make ____ great again" 19 | * We can get most of the way there using the following Twitter query, 20 | * and then we'll use a regex to filter out the false positives. 21 | */ 22 | var query = 'make "great again" -america -filter:retweets'; 23 | var rgx = /make .* great again/i; 24 | 25 | // Runs a Twitter search for the specified `query` and retweets all the results. 26 | function searchAndTweet(succeed, fail) { 27 | console.log("search and tweet"); 28 | client.get('search/tweets', {q: query, count: 15}, function(err, tweets, response) { 29 | if (!tweets.statuses) { 30 | fail(err); 31 | } 32 | 33 | console.log(new Date()); 34 | console.log('found ' + tweets.statuses.length + ' tweets'); 35 | 36 | tweets.statuses.forEach(function(tweet) { 37 | // Make sure we match the regex. 38 | var match = tweet.text.match(rgx); 39 | if (match) { 40 | var tweetId = tweet.id_str; 41 | client.post('statuses/retweet/' + tweetId, function(err, tweet, id) { 42 | // Will return an error if we try to retweet a tweet that we've already 43 | // retweeted. 44 | console.log(err || tweet.text); 45 | }); 46 | } else { 47 | // do something for no match 48 | } 49 | }); 50 | succeed("success"); 51 | }); 52 | } 53 | 54 | exports.handler = function(event, context) { 55 | console.log("inside handler"); 56 | searchAndTweet(context.succeed, context.fail); 57 | } 58 | -------------------------------------------------------------------------------- /2_node_js_aws_lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "make-great-again", 3 | "version": "0.0.0", 4 | "description": "make twitter great again", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "joelgrus@gmail.com", 10 | "license": "BSD-2-Clause", 11 | "dependencies": { 12 | "twitter": "~1.2.5" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /3_python_27_aws_lambda/index.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from twython import Twython 3 | from twython.exceptions import TwythonError 4 | import json 5 | import re 6 | 7 | # credentials.json should look like 8 | # 9 | # { 10 | # "consumer_key": "...", 11 | # "consumer_secret": "...", 12 | # "access_token_key": "...", 13 | # "access_token_secret": "..." 14 | # } 15 | # 16 | # or, if you're feeling dangerous, you can hardcode those values here 17 | with open('credentials.json') as f: 18 | credentials = json.loads(f.read()) 19 | 20 | client = Twython(credentials["consumer_key"], 21 | credentials["consumer_secret"], 22 | credentials["access_token_key"], 23 | credentials["access_token_secret"]) 24 | 25 | rgx = r"make (.*) great again" 26 | query = 'make "great again" -america -filter:retweets' 27 | 28 | def handler(event, context): 29 | results = client.search(q=query) 30 | print("found", len(results["statuses"]), "tweets") 31 | for tweet in results["statuses"]: 32 | text = tweet["text"] 33 | if re.search(rgx, text, re.I): 34 | print(tweet["text"]) 35 | # twitter.retweet will raise an error if we try to retweet a tweet 36 | # that we've already retweeted. to avoid having to keep track, we 37 | # just use a try/except block 38 | try: 39 | client.retweet(id=tweet["id"]) 40 | except TwythonError as e: 41 | print(e) 42 | -------------------------------------------------------------------------------- /4_purescript/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitter", 3 | "version": "1.0.0", 4 | "moduleType": [ 5 | "node" 6 | ], 7 | "ignore": [ 8 | "**/.*", 9 | "node_modules", 10 | "bower_components", 11 | "output" 12 | ], 13 | "dependencies": { 14 | "purescript-console": "^0.1.0", 15 | "purescript-foreign": "~0.7.2", 16 | "purescript-arrays": "~0.4.4", 17 | "purescript-strings": "~0.7.1", 18 | "purescript-functions": "~0.1.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /4_purescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-twitter", 3 | "version": "0.0.0", 4 | "description": "purescript bindings for npm twitter ", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "BSD-2-Clause", 14 | "dependencies": { 15 | "twitter": "~1.2.5", 16 | "fs": "0.0.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /4_purescript/src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | import Control.Monad.Eff 5 | import Control.Monad.Eff.Console 6 | import qualified Data.Array as Array 7 | import Data.Maybe 8 | import qualified Data.String.Regex as Regex 9 | 10 | import MyCredentials 11 | import Twitter 12 | import Twitter.Search 13 | import Twitter.Retweet 14 | import Twitter.Streaming 15 | 16 | findAndRetweet :: SearchOptions -> 17 | Maybe Regex.Regex -> 18 | TwitterClient -> 19 | Eff (twitter :: TWITTER, console :: CONSOLE) Unit 20 | findAndRetweet options rgx client = search client options retweetMatches 21 | where 22 | retweetMatches tweets = do 23 | foreachE (Array.filter rgxFilter tweets) (\tweet -> retweet client tweet.id) 24 | rgxFilter tweet = case rgx of 25 | Just pattern -> Regex.test pattern tweet.text 26 | Nothing -> true 27 | 28 | streamAndLog :: StreamOptions -> TwitterClient -> Eff (console :: CONSOLE, twitter :: TWITTER) Unit 29 | streamAndLog options client = stream client options logTweet logError 30 | where 31 | logTweet tweet = log ("(" ++ tweet.id ++ ") " ++ tweet.user ++ ": " ++ tweet.text) 32 | logError = log 33 | 34 | main :: Eff (console :: CONSOLE, twitter :: TWITTER) Unit 35 | main = twitterClient myCredentials >>= (findAndRetweet options rgx) 36 | where 37 | query = "make \"great again\" -america -filter:retweets" 38 | options = searchOptions query 39 | rgx = Just $ Regex.regex "make (.*) great again" flags 40 | flags = Regex.noFlags { ignoreCase = true } 41 | 42 | --main = twitterClient myCredentials >>= streamAndLog (streamOptions "trump") 43 | -------------------------------------------------------------------------------- /4_purescript/src/Twitter.js: -------------------------------------------------------------------------------- 1 | /* global exports */ 2 | "use strict"; 3 | 4 | // module Twitter 5 | 6 | var Twitter = require('twitter'); 7 | 8 | exports.twitterClient = function(credentials) { 9 | return function() { 10 | return new Twitter(credentials); 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /4_purescript/src/Twitter.purs: -------------------------------------------------------------------------------- 1 | -- | This module defines foreign types and functions for working with 2 | -- | the Twitter library. 3 | 4 | module Twitter where 5 | 6 | import Prelude 7 | import Data.Foreign (Foreign()) 8 | import Control.Monad.Eff (Eff()) 9 | 10 | -- | Effect type for interacting with Twitter. 11 | foreign import data TWITTER :: ! 12 | 13 | -- | The Twitter client returned by the Javascript `Twitter()` constructor. 14 | foreign import data TwitterClient :: * 15 | 16 | -- | Credentials for authenticating to Twitter. 17 | type Credentials = { 18 | consumer_key :: String, 19 | consumer_secret :: String, 20 | access_token_key :: String, 21 | access_token_secret :: String 22 | } 23 | 24 | -- | Use credentials to get a Twitter client. 25 | foreign import twitterClient :: forall eff. Credentials -> Eff (twitter :: TWITTER | eff) TwitterClient 26 | 27 | type TweetId = String 28 | 29 | -- | Record respresenting a tweet. Not complete at all. 30 | type Tweet = { 31 | id :: TweetId, 32 | user :: String, 33 | text :: String 34 | } 35 | 36 | type Tweets = Array Tweet 37 | -------------------------------------------------------------------------------- /4_purescript/src/Twitter/Retweet.js: -------------------------------------------------------------------------------- 1 | /* global exports */ 2 | "use strict"; 3 | 4 | // module Twitter.Retweet 5 | 6 | exports.retweetImpl = function(client, tweetId) { 7 | return function() { 8 | client.post('statuses/retweet/' + tweetId, function(err, tweet, id) { 9 | console.log(err || tweet.text); 10 | }); 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /4_purescript/src/Twitter/Retweet.purs: -------------------------------------------------------------------------------- 1 | -- | Retweeting 2 | 3 | module Twitter.Retweet (retweet) where 4 | 5 | import Prelude (Unit()) 6 | import Data.Function 7 | import Control.Monad.Eff (Eff()) 8 | 9 | import Twitter 10 | 11 | foreign import retweetImpl :: forall eff. Fn2 TwitterClient TweetId (Eff (twitter :: TWITTER | eff) Unit) 12 | 13 | retweet :: forall eff. TwitterClient -> 14 | TweetId -> 15 | Eff (twitter :: TWITTER | eff) Unit 16 | retweet client tweetId = runFn2 retweetImpl client tweetId 17 | -------------------------------------------------------------------------------- /4_purescript/src/Twitter/Search.js: -------------------------------------------------------------------------------- 1 | /* global exports */ 2 | "use strict"; 3 | 4 | // module Twitter.Search 5 | 6 | exports.searchImpl = function(client, searchOptions, callback) { 7 | return function() { 8 | client.get('search/tweets', searchOptions, function(error, tweets, response){ 9 | var results = tweets.statuses.map(function(tweet) { 10 | return { id : tweet.id_str, user : tweet.user.screen_name, text : tweet.text }; 11 | }); 12 | callback(results)(); 13 | return {}; 14 | }); 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /4_purescript/src/Twitter/Search.purs: -------------------------------------------------------------------------------- 1 | -- | The search functionality for the Twitter API. 2 | 3 | module Twitter.Search where 4 | 5 | import Prelude (Unit()) 6 | import Data.Function 7 | import Control.Monad.Eff (Eff()) 8 | 9 | import Twitter 10 | 11 | type SearchOptions = { 12 | q :: String, 13 | count :: Int 14 | } 15 | 16 | searchOptions :: String -> SearchOptions 17 | searchOptions query = { 18 | q : query, 19 | count : 15 20 | } 21 | 22 | foreign import searchImpl :: forall eff. Fn3 23 | TwitterClient 24 | SearchOptions 25 | (Tweets -> Eff (twitter :: TWITTER | eff) Unit) 26 | (Eff (twitter :: TWITTER | eff) Unit) 27 | 28 | search :: forall eff. TwitterClient -> 29 | SearchOptions -> 30 | (Tweets -> Eff (twitter :: TWITTER | eff) Unit) -> 31 | (Eff (twitter :: TWITTER | eff) Unit) 32 | search client options callback = runFn3 searchImpl client options callback 33 | -------------------------------------------------------------------------------- /4_purescript/src/Twitter/Streaming.js: -------------------------------------------------------------------------------- 1 | /* global exports */ 2 | "use strict"; 3 | 4 | // module Twitter.Streaming 5 | 6 | exports.streamImpl = function(client, streamOptions, onData, onError) { 7 | return function() { 8 | client.stream('statuses/filter', streamOptions, function(stream) { 9 | stream.on('data', function(tweet) { 10 | console.log(tweet.text); 11 | onData(tweet); 12 | }); 13 | stream.on('error', onError); 14 | }); 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /4_purescript/src/Twitter/Streaming.purs: -------------------------------------------------------------------------------- 1 | module Twitter.Streaming where 2 | 3 | import Prelude (Unit()) 4 | import Data.Function 5 | import Control.Monad.Eff (Eff()) 6 | 7 | import Twitter 8 | 9 | type StreamError = String 10 | 11 | type StreamOptions = { 12 | track :: String 13 | } 14 | 15 | streamOptions :: String -> StreamOptions 16 | streamOptions q = { track : q } 17 | 18 | foreign import streamImpl :: forall eff. Fn4 TwitterClient 19 | StreamOptions 20 | (Tweet -> Eff (twitter :: TWITTER | eff) Unit) 21 | (StreamError -> Eff (twitter :: TWITTER | eff) Unit) 22 | (Eff (twitter :: TWITTER | eff) Unit) 23 | 24 | 25 | stream :: forall eff. TwitterClient -> 26 | StreamOptions -> 27 | (Tweet -> Eff (twitter :: TWITTER | eff) Unit) -> 28 | (StreamError -> Eff (twitter :: TWITTER | eff) Unit) -> 29 | Eff (twitter :: TWITTER | eff) Unit 30 | stream = runFn4 streamImpl 31 | -------------------------------------------------------------------------------- /4_purescript/test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Control.Monad.Eff.Console 4 | 5 | main = do 6 | log "You should add some tests." 7 | -------------------------------------------------------------------------------- /5_purescript_aws_lambda/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitter", 3 | "version": "1.0.0", 4 | "moduleType": [ 5 | "node" 6 | ], 7 | "ignore": [ 8 | "**/.*", 9 | "node_modules", 10 | "bower_components", 11 | "output" 12 | ], 13 | "dependencies": { 14 | "purescript-console": "^0.1.0", 15 | "purescript-foreign": "~0.7.2", 16 | "purescript-arrays": "~0.4.4", 17 | "purescript-strings": "~0.7.1", 18 | "purescript-functions": "~0.1.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /5_purescript_aws_lambda/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var tweetBot = require('TweetBot') 4 | 5 | exports.handler = function(data, context) { 6 | // ignore the data input. 7 | console.log("inside handler"); 8 | tweetBot.handler(context)(); 9 | }; 10 | -------------------------------------------------------------------------------- /5_purescript_aws_lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-twitter", 3 | "version": "0.0.0", 4 | "description": "purescript bindings for npm twitter ", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "BSD-2-Clause", 14 | "dependencies": { 15 | "twitter": "~1.2.5", 16 | "fs": "0.0.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /5_purescript_aws_lambda/src/AWS/Lambda/Context.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // module AWS.Lambda.Context 4 | 5 | exports.succeed = function(context) { 6 | return function(message) { 7 | return function() { 8 | context.succeed(message); 9 | }; 10 | }; 11 | }; 12 | 13 | exports.fail = function(context) { 14 | return function(message) { 15 | return function() { 16 | context.fail(message); 17 | }; 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /5_purescript_aws_lambda/src/AWS/Lambda/Context.purs: -------------------------------------------------------------------------------- 1 | module AWS.Lambda.Context where 2 | 3 | import Prelude 4 | 5 | import Control.Monad.Eff 6 | 7 | foreign import data Context :: * 8 | 9 | foreign import data LAMBDA :: ! 10 | 11 | foreign import succeed :: forall eff. Context -> String -> Eff (lambda :: LAMBDA | eff) Unit 12 | 13 | foreign import fail :: forall eff. Context -> String -> Eff (lambda :: LAMBDA | eff) Unit 14 | -------------------------------------------------------------------------------- /5_purescript_aws_lambda/src/TweetBot.purs: -------------------------------------------------------------------------------- 1 | module TweetBot where 2 | 3 | import Prelude 4 | import Control.Monad.Eff 5 | import Control.Monad.Eff.Console 6 | import qualified Data.Array as Array 7 | import Data.Maybe 8 | import qualified Data.String.Regex as Regex 9 | 10 | import MyCredentials 11 | import AWS.Lambda.Context 12 | import Twitter 13 | import Twitter.Search 14 | import Twitter.Retweet 15 | import Twitter.Streaming 16 | 17 | findAndRetweet :: SearchOptions -> 18 | Maybe Regex.Regex -> 19 | Context -> 20 | TwitterClient -> 21 | Eff (lambda :: LAMBDA, twitter :: TWITTER, console :: CONSOLE) Unit 22 | findAndRetweet options rgx context client = search client options retweetMatches 23 | where 24 | retweetMatches tweets = do 25 | foreachE (Array.filter rgxFilter tweets) (\tweet -> retweet client tweet.id) 26 | succeed context "success" 27 | rgxFilter tweet = case rgx of 28 | Just pattern -> Regex.test pattern tweet.text 29 | Nothing -> true 30 | 31 | handler :: Context -> Eff (lambda :: LAMBDA, console :: CONSOLE, twitter :: TWITTER) Unit 32 | handler context = twitterClient myCredentials >>= findAndRetweet options rgx context 33 | where 34 | query = "make \"great again\" -america -filter:retweets" 35 | options = searchOptions query 36 | rgx = Just $ Regex.regex "make (.*) great again" flags 37 | flags = Regex.noFlags { ignoreCase = true } 38 | -------------------------------------------------------------------------------- /5_purescript_aws_lambda/src/Twitter.js: -------------------------------------------------------------------------------- 1 | /* global exports */ 2 | "use strict"; 3 | 4 | // module Twitter 5 | 6 | var Twitter = require('twitter'); 7 | 8 | exports.twitterClient = function(credentials) { 9 | return function() { 10 | return new Twitter(credentials); 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /5_purescript_aws_lambda/src/Twitter.purs: -------------------------------------------------------------------------------- 1 | -- | This module defines foreign types and functions for working with 2 | -- | the Twitter library. 3 | 4 | module Twitter where 5 | 6 | import Prelude 7 | import Data.Foreign (Foreign()) 8 | import Control.Monad.Eff (Eff()) 9 | 10 | -- | Effect type for interacting with Twitter. 11 | foreign import data TWITTER :: ! 12 | 13 | -- | The Twitter client returned by the Javascript `Twitter()` constructor. 14 | foreign import data TwitterClient :: * 15 | 16 | -- | Credentials for authenticating to Twitter. 17 | type Credentials = { 18 | consumer_key :: String, 19 | consumer_secret :: String, 20 | access_token_key :: String, 21 | access_token_secret :: String 22 | } 23 | 24 | -- | Use credentials to get a Twitter client. 25 | foreign import twitterClient :: forall eff. Credentials -> Eff (twitter :: TWITTER | eff) TwitterClient 26 | 27 | type TweetId = String 28 | 29 | -- | Record respresenting a tweet. Not complete at all. 30 | type Tweet = { 31 | id :: TweetId, 32 | user :: String, 33 | text :: String 34 | } 35 | 36 | type Tweets = Array Tweet 37 | -------------------------------------------------------------------------------- /5_purescript_aws_lambda/src/Twitter/Retweet.js: -------------------------------------------------------------------------------- 1 | /* global exports */ 2 | "use strict"; 3 | 4 | // module Twitter.Retweet 5 | 6 | exports.retweetImpl = function(client, tweetId) { 7 | return function() { 8 | client.post('statuses/retweet/' + tweetId, function(err, tweet, id) { 9 | console.log(err || tweet.text); 10 | }); 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /5_purescript_aws_lambda/src/Twitter/Retweet.purs: -------------------------------------------------------------------------------- 1 | -- | Retweeting 2 | 3 | module Twitter.Retweet (retweet) where 4 | 5 | import Prelude (Unit()) 6 | import Data.Foreign (Foreign()) 7 | import Data.Function 8 | import Control.Monad.Eff (Eff()) 9 | 10 | import Twitter 11 | 12 | foreign import retweetImpl :: forall eff. Fn2 TwitterClient TweetId (Eff (twitter :: TWITTER | eff) Unit) 13 | 14 | retweet :: forall eff. TwitterClient -> 15 | TweetId -> 16 | Eff (twitter :: TWITTER | eff) Unit 17 | retweet = runFn2 retweetImpl 18 | -------------------------------------------------------------------------------- /5_purescript_aws_lambda/src/Twitter/Search.js: -------------------------------------------------------------------------------- 1 | /* global exports */ 2 | "use strict"; 3 | 4 | // module Twitter.Search 5 | 6 | exports.searchImpl = function(client, searchOptions, callback) { 7 | return function() { 8 | client.get('search/tweets', searchOptions, function(error, tweets, response){ 9 | var results = tweets.statuses.map(function(tweet) { 10 | console.log(tweet.text); 11 | return { id : tweet.id_str, user : tweet.user.screen_name, text : tweet.text }; 12 | }); 13 | callback(results)(); 14 | return {}; 15 | }); 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /5_purescript_aws_lambda/src/Twitter/Search.purs: -------------------------------------------------------------------------------- 1 | -- | The search functionality for the Twitter API. 2 | 3 | module Twitter.Search where 4 | 5 | import Prelude (Unit()) 6 | import Data.Function 7 | import Control.Monad.Eff (Eff()) 8 | 9 | import Twitter 10 | 11 | type SearchOptions = { 12 | q :: String, 13 | count :: Int 14 | } 15 | 16 | searchOptions :: String -> SearchOptions 17 | searchOptions query = { 18 | q : query, 19 | count : 15 20 | } 21 | 22 | foreign import searchImpl :: forall eff. Fn3 23 | TwitterClient 24 | SearchOptions 25 | (Tweets -> Eff (twitter :: TWITTER | eff) Unit) 26 | (Eff (twitter :: TWITTER | eff) Unit) 27 | 28 | search :: forall eff. TwitterClient -> 29 | SearchOptions -> 30 | (Tweets -> Eff (twitter :: TWITTER | eff) Unit) -> 31 | (Eff (twitter :: TWITTER | eff) Unit) 32 | search = runFn3 searchImpl 33 | -------------------------------------------------------------------------------- /5_purescript_aws_lambda/src/Twitter/Streaming.js: -------------------------------------------------------------------------------- 1 | /* global exports */ 2 | "use strict"; 3 | 4 | // module Twitter.Streaming 5 | 6 | exports.streamImpl = function(client, streamOptions, onData, onError) { 7 | return function() { 8 | client.stream('statuses/filter', streamOptions, function(stream) { 9 | stream.on('data', function(tweet) { 10 | console.log(tweet.text); 11 | onData(tweet); 12 | }); 13 | stream.on('error', onError); 14 | }); 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /5_purescript_aws_lambda/src/Twitter/Streaming.purs: -------------------------------------------------------------------------------- 1 | module Twitter.Streaming where 2 | 3 | import Prelude (Unit()) 4 | import Data.Foreign (Foreign()) 5 | import Data.Function 6 | import Control.Monad.Eff (Eff()) 7 | 8 | import Twitter 9 | 10 | type StreamError = String 11 | 12 | type StreamOptions = { 13 | track :: String 14 | } 15 | 16 | streamOptions :: String -> StreamOptions 17 | streamOptions q = { track : q } 18 | 19 | foreign import streamImpl :: forall eff. Fn4 TwitterClient 20 | StreamOptions 21 | (Tweet -> Eff (twitter :: TWITTER | eff) Unit) 22 | (StreamError -> Eff (twitter :: TWITTER | eff) Unit) 23 | (Eff (twitter :: TWITTER | eff) Unit) 24 | 25 | 26 | stream :: forall eff. TwitterClient -> 27 | StreamOptions -> 28 | (Tweet -> Eff (twitter :: TWITTER | eff) Unit) -> 29 | (StreamError -> Eff (twitter :: TWITTER | eff) Unit) -> 30 | Eff (twitter :: TWITTER | eff) Unit 31 | stream = runFn4 streamImpl 32 | -------------------------------------------------------------------------------- /5_purescript_aws_lambda/test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Control.Monad.Eff.Console 4 | 5 | main = do 6 | log "You should add some tests." 7 | -------------------------------------------------------------------------------- /5_purescript_aws_lambda/twitter.js: -------------------------------------------------------------------------------- 1 | // Generated by psc-bundle 0.7.3.0 2 | var PS = { }; 3 | (function(exports) { 4 | /* global exports */ 5 | "use strict"; 6 | 7 | exports.foreachE = function (as) { 8 | return function (f) { 9 | return function () { 10 | for (var i = 0, l = as.length; i < l; i++) { 11 | f(as[i])(); 12 | } 13 | }; 14 | }; 15 | }; 16 | 17 | })(PS["Control.Monad.Eff"] = PS["Control.Monad.Eff"] || {}); 18 | (function(exports) { 19 | // Generated by psc version 0.7.3.0 20 | "use strict"; 21 | var $foreign = PS["Control.Monad.Eff"]; 22 | var Prelude = PS["Prelude"]; 23 | exports["foreachE"] = $foreign.foreachE;; 24 | 25 | })(PS["Control.Monad.Eff"] = PS["Control.Monad.Eff"] || {}); 26 | (function(exports) { 27 | /* global exports, console */ 28 | "use strict"; 29 | 30 | // module Control.Monad.Eff.Console 31 | 32 | exports.log = function (s) { 33 | return function () { 34 | console.log(s); 35 | return {}; 36 | }; 37 | }; 38 | 39 | })(PS["Control.Monad.Eff.Console"] = PS["Control.Monad.Eff.Console"] || {}); 40 | (function(exports) { 41 | // Generated by psc version 0.7.3.0 42 | "use strict"; 43 | var $foreign = PS["Control.Monad.Eff.Console"]; 44 | var Prelude = PS["Prelude"]; 45 | var Control_Monad_Eff = PS["Control.Monad.Eff"]; 46 | exports["log"] = $foreign.log;; 47 | 48 | })(PS["Control.Monad.Eff.Console"] = PS["Control.Monad.Eff.Console"] || {}); 49 | (function(exports) { 50 | /* global exports */ 51 | "use strict"; 52 | 53 | // module Control.Monad.Eff.Twitter 54 | 55 | 56 | var Twitter = require('twitter'); 57 | 58 | exports.twitterClient = function(credentials) { 59 | return function() { 60 | return new Twitter(credentials); 61 | }; 62 | }; 63 | 64 | // This is a pretty bad hack to get rid of Nothing values and mimic fromJust. 65 | function cleanOptions(dirtyOptions) { 66 | var options = {}; 67 | for (var key in dirtyOptions) { 68 | var value = dirtyOptions[key]; 69 | if (typeof value != 'object') { 70 | options[key] = value; 71 | } else { 72 | if (value['value0']) { 73 | options[key] = value['value0']; 74 | } 75 | } 76 | } 77 | console.log(options); 78 | return options; 79 | } 80 | 81 | exports.search = function(client) { 82 | return function(searchOptions) { 83 | return function(callback) { 84 | return function() { 85 | client.get('search/tweets', cleanOptions(searchOptions), function(error, tweets, response){ 86 | var results = []; 87 | if (tweets.statuses) { 88 | tweets.statuses.forEach(function(tweet) { 89 | results.push({ 90 | id : tweet.id_str, 91 | user : tweet.user.screen_name, 92 | text: tweet.text 93 | }); 94 | }); 95 | } 96 | callback(results)(); 97 | }); 98 | }; 99 | }; 100 | }; 101 | }; 102 | 103 | exports.retweet = function(client) { 104 | return function(tweetId) { 105 | return function() { 106 | client.post('statuses/retweet/' + tweetId, function(err, tweet, id) { 107 | console.log(err || tweet.text); 108 | }); 109 | }; 110 | }; 111 | }; 112 | 113 | })(PS["Control.Monad.Eff.Twitter"] = PS["Control.Monad.Eff.Twitter"] || {}); 114 | (function(exports) { 115 | // Generated by psc version 0.7.3.0 116 | "use strict"; 117 | var Prelude = PS["Prelude"]; 118 | var Data_Functor_Invariant = PS["Data.Functor.Invariant"]; 119 | var Control_Alt = PS["Control.Alt"]; 120 | var Control_Alternative = PS["Control.Alternative"]; 121 | var Control_Extend = PS["Control.Extend"]; 122 | var Control_MonadPlus = PS["Control.MonadPlus"]; 123 | var Control_Plus = PS["Control.Plus"]; 124 | var Data_Monoid = PS["Data.Monoid"]; 125 | var Nothing = (function () { 126 | function Nothing() { 127 | 128 | }; 129 | Nothing.value = new Nothing(); 130 | return Nothing; 131 | })(); 132 | var Just = (function () { 133 | function Just(value0) { 134 | this.value0 = value0; 135 | }; 136 | Just.create = function (value0) { 137 | return new Just(value0); 138 | }; 139 | return Just; 140 | })(); 141 | exports["Nothing"] = Nothing; 142 | exports["Just"] = Just;; 143 | 144 | })(PS["Data.Maybe"] = PS["Data.Maybe"] || {}); 145 | (function(exports) { 146 | // Generated by psc version 0.7.3.0 147 | "use strict"; 148 | var $foreign = PS["Control.Monad.Eff.Twitter"]; 149 | var Prelude = PS["Prelude"]; 150 | var Data_Foreign = PS["Data.Foreign"]; 151 | var Data_Maybe = PS["Data.Maybe"]; 152 | var Control_Monad_Eff = PS["Control.Monad.Eff"]; 153 | var searchOptions = function (query) { 154 | return { 155 | q: query, 156 | geocode: Data_Maybe.Nothing.value, 157 | lang: Data_Maybe.Nothing.value, 158 | locale: Data_Maybe.Nothing.value, 159 | result_type: Data_Maybe.Nothing.value, 160 | count: Data_Maybe.Nothing.value, 161 | until: Data_Maybe.Nothing.value, 162 | since_id: Data_Maybe.Nothing.value, 163 | max_id: Data_Maybe.Nothing.value, 164 | include_entities: Data_Maybe.Nothing.value 165 | }; 166 | }; 167 | exports["searchOptions"] = searchOptions; 168 | exports["retweet"] = $foreign.retweet; 169 | exports["search"] = $foreign.search; 170 | exports["twitterClient"] = $foreign.twitterClient;; 171 | 172 | })(PS["Control.Monad.Eff.Twitter"] = PS["Control.Monad.Eff.Twitter"] || {}); 173 | (function(exports) { 174 | /* global exports */ 175 | "use strict"; 176 | 177 | exports.filter = function (f) { 178 | return function (xs) { 179 | return xs.filter(f); 180 | }; 181 | }; 182 | 183 | })(PS["Data.Array"] = PS["Data.Array"] || {}); 184 | (function(exports) { 185 | // Generated by psc version 0.7.3.0 186 | "use strict"; 187 | var $foreign = PS["Data.Array"]; 188 | var Prelude = PS["Prelude"]; 189 | var Data_Traversable = PS["Data.Traversable"]; 190 | var Control_Lazy = PS["Control.Lazy"]; 191 | var Control_Alt = PS["Control.Alt"]; 192 | var Data_Maybe = PS["Data.Maybe"]; 193 | var Data_Maybe_Unsafe = PS["Data.Maybe.Unsafe"]; 194 | var Data_Foldable = PS["Data.Foldable"]; 195 | var Control_Alternative = PS["Control.Alternative"]; 196 | var Control_MonadPlus = PS["Control.MonadPlus"]; 197 | var Control_Plus = PS["Control.Plus"]; 198 | var Data_Functor_Invariant = PS["Data.Functor.Invariant"]; 199 | var Data_Monoid = PS["Data.Monoid"]; 200 | var Data_Tuple = PS["Data.Tuple"]; 201 | exports["filter"] = $foreign.filter;; 202 | 203 | })(PS["Data.Array"] = PS["Data.Array"] || {}); 204 | (function(exports) { 205 | /* global exports */ 206 | "use strict"; 207 | 208 | exports["regex'"] = function (s1) { 209 | return function (s2) { 210 | return new RegExp(s1, s2); 211 | }; 212 | }; 213 | 214 | exports.test = function (r) { 215 | return function (s) { 216 | return r.test(s); 217 | }; 218 | }; 219 | 220 | })(PS["Data.String.Regex"] = PS["Data.String.Regex"] || {}); 221 | (function(exports) { 222 | // Generated by psc version 0.7.3.0 223 | "use strict"; 224 | var $foreign = PS["Data.String.Regex"]; 225 | var Prelude = PS["Prelude"]; 226 | var Data_String = PS["Data.String"]; 227 | var Data_Maybe = PS["Data.Maybe"]; 228 | var renderFlags = function (f) { 229 | return (function () { 230 | if (f.global) { 231 | return "g"; 232 | }; 233 | if (!f.global) { 234 | return ""; 235 | }; 236 | throw new Error("Failed pattern match at Data.String.Regex line 63, column 1 - line 64, column 1: " + [ f.global.constructor.name ]); 237 | })() + ((function () { 238 | if (f.ignoreCase) { 239 | return "i"; 240 | }; 241 | if (!f.ignoreCase) { 242 | return ""; 243 | }; 244 | throw new Error("Failed pattern match at Data.String.Regex line 63, column 1 - line 64, column 1: " + [ f.ignoreCase.constructor.name ]); 245 | })() + ((function () { 246 | if (f.multiline) { 247 | return "m"; 248 | }; 249 | if (!f.multiline) { 250 | return ""; 251 | }; 252 | throw new Error("Failed pattern match at Data.String.Regex line 63, column 1 - line 64, column 1: " + [ f.multiline.constructor.name ]); 253 | })() + ((function () { 254 | if (f.sticky) { 255 | return "y"; 256 | }; 257 | if (!f.sticky) { 258 | return ""; 259 | }; 260 | throw new Error("Failed pattern match at Data.String.Regex line 63, column 1 - line 64, column 1: " + [ f.sticky.constructor.name ]); 261 | })() + (function () { 262 | if (f.unicode) { 263 | return "u"; 264 | }; 265 | if (!f.unicode) { 266 | return ""; 267 | }; 268 | throw new Error("Failed pattern match at Data.String.Regex line 63, column 1 - line 64, column 1: " + [ f.unicode.constructor.name ]); 269 | })()))); 270 | }; 271 | var regex = function (s) { 272 | return function (f) { 273 | return $foreign["regex'"](s)(renderFlags(f)); 274 | }; 275 | }; 276 | var noFlags = { 277 | global: false, 278 | ignoreCase: false, 279 | multiline: false, 280 | sticky: false, 281 | unicode: false 282 | }; 283 | exports["noFlags"] = noFlags; 284 | exports["renderFlags"] = renderFlags; 285 | exports["regex"] = regex; 286 | exports["test"] = $foreign.test;; 287 | 288 | })(PS["Data.String.Regex"] = PS["Data.String.Regex"] || {}); 289 | (function(exports) { 290 | // Generated by psc version 0.7.3.0 291 | "use strict"; 292 | var Control_Monad_Eff_Twitter = PS["Control.Monad.Eff.Twitter"]; 293 | var myCredentials = { 294 | consumer_key: "vBEnTMuHpHPbS833D1Pvy2t0x", 295 | consumer_secret: "Hpu7ba3NOtq2ZYDOwkpmzR9l7K1BCuewbs9XBk57AsqcHUqv5P", 296 | access_token_key: "4635054085-iOkHyBYGdQz3q8aGJaUWSawfYNgWmPXvalKyoAt", 297 | access_token_secret: "wKEza6TbtFrWSXYYtV5voisEkK7g1STldl1eF5pNMKV32" 298 | }; 299 | exports["myCredentials"] = myCredentials;; 300 | 301 | })(PS["MyCredentials"] = PS["MyCredentials"] || {}); 302 | (function(exports) { 303 | // Generated by psc version 0.7.3.0 304 | "use strict"; 305 | var Data_Array = PS["Data.Array"]; 306 | var Data_String_Regex = PS["Data.String.Regex"]; 307 | var Prelude = PS["Prelude"]; 308 | var Control_Monad_Eff = PS["Control.Monad.Eff"]; 309 | var Control_Monad_Eff_Twitter = PS["Control.Monad.Eff.Twitter"]; 310 | var Control_Monad_Eff_Console = PS["Control.Monad.Eff.Console"]; 311 | var MyCredentials = PS["MyCredentials"]; 312 | var Control_Monad = PS["Control.Monad"]; 313 | var Data_Maybe = PS["Data.Maybe"]; 314 | var retweetAll = function (client) { 315 | return function (regex) { 316 | return function (tweets) { 317 | var matchingTweets = (function () { 318 | if (regex instanceof Data_Maybe.Just) { 319 | return Data_Array.filter(function (tweet) { 320 | return Data_String_Regex.test(regex.value0)(tweet.text); 321 | })(tweets); 322 | }; 323 | if (regex instanceof Data_Maybe.Nothing) { 324 | return tweets; 325 | }; 326 | throw new Error("Failed pattern match at Main line 20, column 5 - line 24, column 1: " + [ regex.constructor.name ]); 327 | })(); 328 | return Control_Monad_Eff.foreachE(matchingTweets)(function (tweet) { 329 | return function __do() { 330 | Control_Monad_Eff_Twitter.retweet(client)(tweet.id)(); 331 | return Control_Monad_Eff_Console.log(tweet.text)(); 332 | }; 333 | }); 334 | }; 335 | }; 336 | }; 337 | var main = (function () { 338 | var options = (function () { 339 | var _3 = {}; 340 | for (var _4 in Control_Monad_Eff_Twitter.searchOptions("make \"great again\" -america -filter:retweets")) { 341 | if ((Control_Monad_Eff_Twitter.searchOptions("make \"great again\" -america -filter:retweets")).hasOwnProperty(_4)) { 342 | _3[_4] = (Control_Monad_Eff_Twitter.searchOptions("make \"great again\" -america -filter:retweets"))[_4]; 343 | }; 344 | }; 345 | _3.count = new Data_Maybe.Just(3); 346 | return _3; 347 | })(); 348 | var flags = (function () { 349 | var _5 = {}; 350 | for (var _6 in Data_String_Regex.noFlags) { 351 | if (Data_String_Regex.noFlags.hasOwnProperty(_6)) { 352 | _5[_6] = Data_String_Regex.noFlags[_6]; 353 | }; 354 | }; 355 | _5.ignoreCase = true; 356 | return _5; 357 | })(); 358 | var regex = Data_Maybe.Just.create(Data_String_Regex.regex("make (.*) great again")(flags)); 359 | return function __do() { 360 | var _0 = Control_Monad_Eff_Twitter.twitterClient(MyCredentials.myCredentials)(); 361 | return Control_Monad_Eff_Twitter.search(_0)(options)(retweetAll(_0)(regex))(); 362 | }; 363 | })(); 364 | exports["main"] = main; 365 | exports["retweetAll"] = retweetAll;; 366 | 367 | })(PS["Main"] = PS["Main"] || {}); 368 | 369 | PS["Main"].main(); 370 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # polyglot-twitter-bot 2 | code for writing twitter bots in several languages 3 | 4 | * Part 1: http://joelgrus.com/2015/12/29/polyglot-twitter-bot-part-1-nodejs/ 5 | * Part 2: http://joelgrus.com/2015/12/29/polyglot-twitter-bot-part-2-nodejs-aws-lambda/ 6 | * Part 3: http://joelgrus.com/2015/12/30/polyglot-twitter-bot-part-3-python-27-aws-lambda/ 7 | * Part 4: http://joelgrus.com/2015/12/31/polyglot-twitter-bot-part-4-purescript/ 8 | --------------------------------------------------------------------------------