├── .gitignore ├── README.md ├── botfiles ├── package.json ├── sample-text.js └── temp-config.js ├── gulpfile.js ├── index.js ├── package.json ├── temp-lambda-config.js └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | #config 2 | botfiles/config.js 3 | lambda-config.js 4 | node_modules/* 5 | 6 | #OS junk files 7 | *.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Blank AWS Lambda Bot 2 | 3 | ##Instructions 4 | There's a more detailed writeup [here.](https://medium.com/@emckean/create-a-simple-free-text-driven-twitterbot-with-aws-lambda-node-js-b80e26209f5#.fwl8p9ikf) 5 | 6 | Prerequisites: I assume you have node and npm installed. Need help installing either? [Here's a link for you.](http://blog.npmjs.org/post/85484771375/how-to-install-npm) 7 | 8 | 1. Clone this repo 9 | 2. Run `npm install` 10 | 3. Open in your favorite editor. 11 | 12 | ##Configuration 13 | 14 | To run this bot yourself you will need Twitter and AWS credentials. 15 | 16 | #####For Twitter, you will need: 17 | 18 | * a Twitter account 19 | * a Twitter application (not the same as a Twitter account; apply for one at apps.twitter.com) 20 | * a Consumer Key, a Consumer Secret, an Access Token, and your Access Token Secret 21 | 22 | Put this information in the temp-config.js file, and rename it to config.js. 23 | 24 | ######For AWS, you will need: 25 | 26 | * an AWS account (you'll need to know your AWS account number) 27 | * a new AWS user for this bot (and its Access Key ID and the Secret Access Key) 28 | * the [AWS CLI installed](http://docs.aws.amazon.com/cli/latest/userguide/installing.html) 29 | * a local profile configured to use your new user's credentials 30 | 31 | Attach the AWSLambdaFullAccess Policy to your new user. [Need help setting up an AWS account or user?](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html) 32 | 33 | ######Configuring for Lambda 34 | 35 | Open the temp-lambda-config.js file and update the 'profile', 'role', 'region', and 'function name' fields. Rename this file to lambda-config.js. 36 | 37 | If you change the name of the index.js file, you'll need to update it in both the lambda-config.js file and the gulpfile. 38 | 39 | ######Tests 40 | There are tests for the functions in index.js in the tests/test.js file. Run them with `npm test`. 41 | 42 | ##Source Data 43 | 44 | To add text for your bot to tweet, add it in the botfiles/sample-text.js directory. 45 | 46 | IMPORTANT: you must install the botfiles folder as a local module before deploying: 47 | 48 | `npm install --save ./YOURPATH/botfiles` 49 | 50 | If you make changes, you must update the installation: 51 | 52 | `rm -rf node_module/botfiles && npm install /YOURPATH/botfiles` 53 | 54 | ##Deploy 55 | 56 | To deploy, run the tests (and also `gulp lint`). When you're happy with that output, run `gulp deploy`. 57 | 58 | ##Lambda 59 | Your function will now be available in the Lambda console! You can configure a test event there (use '{}') and try it out! 60 | 61 | You can also set up your bot to run on a schedule using the "Event Sources" tab in the Lambda function console. 62 | 63 | ##Thank Yous 64 | Thanks to [Darius Kazemi](https://github.com/dariusk) for the wordfilter module and for the .pick function. Thanks to [Thoughtworks](https://github.com/ThoughtWorksStudios) for the wonderful [node-aws-lambda](https://github.com/ThoughtWorksStudios/node-aws-lambda) module. Thanks also to [Allison Parrish](https://twitter.com/aparrish) and David Celis for reviewing; all errors remain the exclusive property & reponsibility of the author. :-) 65 | -------------------------------------------------------------------------------- /botfiles/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botfiles", 3 | "version": "1.0.1", 4 | "description": "config and data files for lambda bot" 5 | } -------------------------------------------------------------------------------- /botfiles/sample-text.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | //include your tweetable lines in this file, e.g. 3 | "I like bots!", 4 | "Here's another thing to tweet!" 5 | ] 6 | -------------------------------------------------------------------------------- /botfiles/temp-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | //Your Twitter credentials go in this file 3 | consumer_key: '', 4 | consumer_secret: '', 5 | access_token: '', 6 | access_token_secret: '' 7 | }; 8 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var zip = require('gulp-zip'); 3 | var del = require('del'); 4 | var install = require('gulp-install'); 5 | var runSequence = require('run-sequence'); 6 | var awsLambda = require("node-aws-lambda"); 7 | var jshint = require('gulp-jshint'); 8 | 9 | gulp.task('clean', function() { 10 | return del(['./dist', './dist.zip']); 11 | }); 12 | 13 | gulp.task('js', function() { 14 | //this is the name of your main function file 15 | return gulp.src('index.js') 16 | .pipe(gulp.dest('dist/')); 17 | }); 18 | 19 | gulp.task('node-mods', function() { 20 | return gulp.src('./package.json') 21 | .pipe(gulp.dest('dist/')) 22 | .pipe(install({production: true})); 23 | }); 24 | 25 | gulp.task('zip', function() { 26 | return gulp.src(['dist/**/*', '!dist/package.json']) 27 | .pipe(zip('dist.zip')) 28 | .pipe(gulp.dest('./')); 29 | }); 30 | //this task doesn't run in the deploy pipeline 31 | gulp.task('lint', function() { 32 | return gulp.src('index.js') 33 | .pipe(jshint()) 34 | .pipe(jshint.reporter('default')); 35 | }); 36 | 37 | gulp.task('upload', function(callback) { 38 | awsLambda.deploy('./dist.zip', require("./lambda-config.js"), callback); 39 | }); 40 | 41 | gulp.task('deploy', function(callback) { 42 | return runSequence( 43 | ['clean'], 44 | ['js', 'node-mods'], 45 | ['zip'], 46 | ['upload'], 47 | callback 48 | ); 49 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Twit = require('twit'); 2 | var wordfilter = require('wordfilter'); 3 | var T = new Twit(require('botfiles/config.js')); 4 | var myText = require('botfiles/sample-text.js'); 5 | 6 | //a nice 'pick' function thanks to Darius Kazemi: https://github.com/dariusk 7 | Array.prototype.pick = function() { 8 | return this[Math.floor(Math.random()*this.length)]; 9 | }; 10 | 11 | //functions 12 | function tweetOK(phrase) { 13 | if (!wordfilter.blacklisted(phrase) && phrase !== undefined && phrase !== "" && tweetLengthOK(phrase)){ 14 | return true; 15 | } else { 16 | return false; 17 | } 18 | } 19 | 20 | function tweetLengthOK(phrase) { 21 | if (phrase.length <= 130){ 22 | return true; 23 | } else { 24 | return false; 25 | } 26 | } 27 | 28 | function pickTweet(){ 29 | var tweetText = myText.pick(); 30 | if (tweetOK(tweetText)) { 31 | return tweetText; 32 | } 33 | else { 34 | tweetText = pickTweet(); 35 | } 36 | } 37 | 38 | exports.handler = function myBot(event, context) { 39 | 40 | var textToTweet = pickTweet(); 41 | 42 | T.post('statuses/update', { status: textToTweet }, function(err, reply) { 43 | if (err) { 44 | console.log('error:', err); 45 | context.fail(); 46 | } 47 | else { 48 | console.log('tweet:', reply); 49 | context.succeed(); 50 | } 51 | }); 52 | }; 53 | 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blank-lambda-bot", 3 | "version": "1.0.0", 4 | "description": "a blank lambda-bot template", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "keywords": [ 10 | "aws", 11 | "lambda", 12 | "bot", 13 | "template" 14 | ], 15 | "author": "Erin McKean", 16 | "license": "ISC", 17 | "repository": "https://github.com/emckean/blank-lambda-bot", 18 | "devDependencies": { 19 | "chai": "^3.5.0", 20 | "del": "^2.2.0", 21 | "gulp": "^3.9.1", 22 | "gulp-install": "^0.6.0", 23 | "gulp-jshint": "^2.0.1", 24 | "gulp-zip": "^3.2.0", 25 | "jshint": "^2.9.2", 26 | "mocha": "^2.4.5", 27 | "node-aws-lambda": "^0.1.8", 28 | "rewire": "^2.5.1", 29 | "run-sequence": "^1.2.1" 30 | }, 31 | "dependencies": { 32 | "botfiles": "file:///Users/emckean/Code/blank-lambda-bot/botfiles", 33 | "twit": "^2.2.4", 34 | "underscore": "^1.8.3", 35 | "wordfilter": "^0.2.6" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /temp-lambda-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | profile: '', // load your AWS credentials from a custom profile 3 | region: 'us-west-2', //the region of your Lambda function 4 | handler: 'index.handler', //the name of the handler function: index because the main file is index.js 5 | role: 'arn:aws:iam::YOURACCOUNTHERE:role/lambda_basic_execution', // the Lambda role 6 | functionName: '', //name 7 | timeout: 10, 8 | memorySize: 128, 9 | publish: true, // this creates a new version of your Lambda function every time you update it 10 | runtime: 'nodejs', // for node 10, otherwise use 'nodejs4.3' 11 | } 12 | 13 | //see other options here: https://github.com/ThoughtWorksStudios/node-aws-lambda -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var rewire = require("rewire"); 3 | var bot = rewire('../index.js'); 4 | 5 | //rewiring 6 | pickTweet = bot.__get__('pickTweet'); 7 | tweetOK = bot.__get__('tweetOK'); 8 | tweetLengthOK = bot.__get__('tweetLengthOK'); 9 | 10 | 11 | describe('canary test', function() { 12 | it ('should pass this canary test', function(){ 13 | expect(true).to.eql(true); 14 | }); 15 | }); 16 | 17 | 18 | describe('pick phrase test', function() { 19 | it ('it should return a phrase', function(){ 20 | var phrase = pickTweet(); 21 | expect(phrase).to.not.eql(undefined); 22 | }); 23 | }); 24 | 25 | 26 | describe('phrase length tests', function() { 27 | it ('it should return true if phrase is = 130 char', function(){ 28 | var testPhrase = 'This test text has been hand-crafted with love to equal exactly one hundred & thirty of the finest Tweetable characters available!'; 29 | var phrase = tweetLengthOK(testPhrase); 30 | expect(phrase).to.eql(true); 31 | }); 32 | 33 | it ('it should return true if phrase is <= 130 char', function(){ 34 | var phrase = tweetLengthOK('Hi! I am a short phrase!'); 35 | expect(phrase).to.eql(true); 36 | }); 37 | 38 | it ('it should return false if phrase is >= 130 char', function(){ 39 | var testPhrase = 'This text is far, far, far too long to tweet, if you want to stay well under the Twitter 140-character limit, which you do, I believe, yes?'; 40 | var phrase = tweetLengthOK(testPhrase); 41 | expect(phrase).to.eql(false); 42 | }); 43 | }); 44 | 45 | describe('word filter test', function() { 46 | it ('it should return an error if the phrase contains a bad word', function(){ 47 | var phrase = "using the word 'lame' is lame"; 48 | var phraseCheck = tweetOK(phrase); 49 | expect(phraseCheck).to.eql(false); 50 | }); 51 | 52 | it ('it should return an error if the phrase is undefined', function(){ 53 | var phrase; 54 | var phraseCheck = tweetOK(phrase); 55 | expect(phraseCheck).to.eql(false); 56 | }); 57 | 58 | it ('it should return an error if the phrase is empty', function(){ 59 | var phrase = ""; 60 | var phraseCheck = tweetOK(phrase); 61 | expect(phraseCheck).to.eql(false); 62 | }); 63 | 64 | it ('it should return true if the phrase is OK', function(){ 65 | var phrase = "Hi! I am a short phrase that is perfectly fine."; 66 | var phraseCheck = tweetOK(phrase); 67 | expect(phraseCheck).to.eql(true); 68 | }); 69 | 70 | }); --------------------------------------------------------------------------------