Skip to content

Instantly share code, notes, and snippets.

@aparrish
Created February 1, 2017 02:50
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aparrish/110612483c2aab5c78a6cbd1e0d6403f to your computer and use it in GitHub Desktop.
Save aparrish/110612483c2aab5c78a6cbd1e0d6403f to your computer and use it in GitHub Desktop.
Bot Basics in Node
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Bot basics in Node\n",
"\n",
"[Allison Parrish](http://www.decontextualize.com/) for [Twitter Bot Workshop](http://botshop.decontextualize.com/).\n",
"\n",
"This tutorial describes how to make a simple \"every\" style bot (i.e., a bot that exhausts a list) in Node, along with basic instructions on how to deploy the bot to the cloud. [Go through this tutorial first](http://creative-coding.decontextualize.com/node/).\n",
"\n",
"By the end of the tutorial, you'll have a bot that tweets the names of every US city. You'll need to have beginner to intermediary programming skills in Javascript to follow along.\n",
"\n",
"Note while reading this tutorial: This is a transcript of an [IJavascript Notebook](https://www.npmjs.com/package/ijavascript) session. You can follow along by copying and pasting the code into a Node interactive session. Where the `Out` sections read `undefined`, that's just because the expression inside the cell above didn't evaluate to anything (as with, e.g., assignments).\n",
"\n",
"## Parsing JSON\n",
"\n",
"Say you have some JSON data in a string, like so:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"undefined"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"var someJsonData = \"{\\\"a\\\": 1, \\\"b\\\": 2}\";"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You can use the `JSON.parse()` function to turn this string into an actual Javascript data structure."
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"collapsed": false,
"scrolled": true
},
"outputs": [
{
"data": {
"text/plain": [
"1"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"var dataObject = JSON.parse(someJsonData);\n",
"dataObject[\"a\"];"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"It doesn't matter where you get your JSON data. If you've got it in a variable (as a string or a [buffer](https://nodejs.org/api/fs.html#fs_buffer_api)), `JSON.parse()` will turn it into a Javascript object."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Loading data from a file\n",
"\n",
"Let's say I've got some JSON data on my computer, maybe in the same directory as the program I'm running. For example, I just The question then arises: how do I turn that file on my computer into something inside my Javascript program? The answer: the `fs` module. The `fs` module has a number of functions that make it possible to take a file from your computer and load its contents into a variable in your program.\n",
"\n",
"To use the `fs` module, first `require` it:"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"undefined"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"var fs = require('fs');"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Once you've done this, you can use `fs.readFileSync()` to read a file from your computer into a variable. Put the name of the file in a string as the only parameter to the function, like so:"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"undefined"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"var citiesFileData = fs.readFileSync(\"us_cities.json\");"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The `citiesFileData` variable now contains a \"buffer\" object with the bytes from the file. The `JSON.parse()` function (which comes for free with Javascript), you can turn that buffer into a Javascript object."
]
},
{
"cell_type": "code",
"execution_count": 28,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"undefined"
]
},
"execution_count": 28,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"var citiesData = JSON.parse(citiesFileData);"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now, you can access the data in the original JSON file using regular Javascript syntax:"
]
},
{
"cell_type": "code",
"execution_count": 29,
"metadata": {
"collapsed": false,
"scrolled": true
},
"outputs": [
{
"data": {
"text/plain": [
"{ city: 'New York', state: 'New York', population: '8,405,837' }"
]
},
"execution_count": 29,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"cities[\"cities\"][0]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The following line of code does all of the above in one step, without assigning the buffer to a separate variable first."
]
},
{
"cell_type": "code",
"execution_count": 31,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"undefined"
]
},
"execution_count": 31,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"var citiesData = JSON.parse(fs.readFileSync(\"us_cities.json\"))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"For the sake of convenience in the code that follows, I'm going to make a new variable with *just* the value for the `cities` key in the original data:"
]
},
{
"cell_type": "code",
"execution_count": 32,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"undefined"
]
},
"execution_count": 32,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"var cities = citiesData[\"cities\"];"
]
},
{
"cell_type": "code",
"execution_count": 40,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"'New York'"
]
},
"execution_count": 40,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"cities[0][\"city\"]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Sending a tweet\n",
"\n",
"The easiest way to send a tweet is to install a Node library that provides an interface to the Twitter API. In this class, we'll be using the [node-twitter-api package](https://www.npmjs.com/package/node-twitter-api), which you can install on the command line with the following command:\n",
"\n",
" npm install node-twitter-api --save\n",
" \n",
"(Make sure to initialize the project in your current directory by running `npm init` first.)\n",
"\n",
"After you install the library, use the `require` function to load it:"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"undefined"
]
},
"execution_count": 20,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"var twitterAPI = require('node-twitter-api');"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Authorization\n",
"\n",
"All requests to the REST API—making a post, running a search, following or unfollowing an account—must be made on behalf of a user. Your code must be authorized to submit requests to the API on that user's behalf. A user can authorize code to act on their behalf through the medium of something called an application. You can see which applications you've authorized by [logging into Twitter and following this link](https://twitter.com/settings/applications).\n",
"\n",
"When making requests to the Twitter API, you don't use your username and password: instead you use four different unique identifiers: the Consumer (Application) Key, the Consumer (Application) Secret, an Access Token, and a Token Secret. The Consumer Key/Secret identify the application, and the Token/Token Secret identify a particular account as having access through a particular application. You don't choose these values; they're strings of random numbers automatically generated by Twitter. These strings, together, act as a sort of \"password\" for the Twitter API.\n",
"\n",
"In order to obtain these four magical strings, we need to...\n",
"\n",
"* Create a Twitter \"application,\" which is associated with a Twitter account; the \"API Key\" and \"API Secret\" are created with this application;\n",
"* Create an access token and token secret;\n",
"* Copy all four strings to use when creating our computer programs that access Twitter.\n",
"\n",
"Posting to Twitter requires two steps. First, you need to get *credentials* for the user on whose behalf you want to make the post. In order to do this, you need to create a Twitter application and then have a Twitter user (i.e., your bot's user account) *authorize* the application. [Here's a good overview of some of the steps required to do this](https://iag.me/socialmedia/how-to-create-a-twitter-app-in-8-easy-steps/), and we'll go through it in class as well.\n",
"\n",
"Once you've created the application, you need to authorize a particular user to use it. To do this, download [this script](https://raw.githubusercontent.com/aparrish/example-twitter-bot-node/master/get_tokens.js) to a Node project directory where you've already installed `node-twitter-api` and run it like so:\n",
"\n",
" node get_tokens.js\n",
" \n",
"Then, simply follow the instructions, using the application key and application secret from the Twitter application you just created. Make sure you're logged into Twitter with your bot's account!\n",
"\n",
"Copy down the resulting access token and token secret. The good news about the Twitter API is that these tokens *never expire*, so you should be able to use them in perpetuity.\n",
"\n",
"### Making the post\n",
"\n",
"To actually post something to Twitter, you need to do the following:\n",
"\n",
"* Initialize a Twitter API object with `new`\n",
"* Call the `statuses` method of the resulting object with the appropriate parameters.\n",
"\n",
"Here's what that looks like. You can treat this code as a template, replacing only the relevant portions:"
]
},
{
"cell_type": "code",
"execution_count": 26,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"undefined"
]
},
"execution_count": 26,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"var yourConsumerKey = \"\";\n",
"var yourConsumerSecret = \"\";\n",
"var yourAccessToken = \"\";\n",
"var yourTokenSecret = \"\";\n",
"\n",
"var twitter = new twitterAPI({\n",
" consumerKey: yourConsumerKey,\n",
" consumerSecret: yourConsumerSecret});\n",
"\n",
"twitter.statuses(\n",
" \"update\",\n",
" {\"status\": \"your tweet text\"},\n",
" yourAccessToken,\n",
" yourTokenSecret,\n",
" function(error, data, response) {\n",
" if (error) {\n",
" console.log(\"something went wrong: \", error);\n",
" }\n",
" }\n",
");"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You'll need to fill in the `yourConsumerKey`, `yourConsumerSecret`, `yourAccessToken` and `yourTokenSecret` variables using the values you obtained from authorizing the application. If everything went according to plan, you should see a new tweet on the timeline of the user you just authorized. (The text will be whatever you put in for `your tweet text`.) If something went wrong, you'll see an error message displayed in your terminal window.\n",
"\n",
"Once you've called `new twitterAPI(...)` you don't need to call it again. You can reuse the same object for multiple requests to `statuses`.\n",
"\n",
"### Tweeting data\n",
"\n",
"It's simple to take the code above and use it to tweet data from `us_cities.json`. To do this, just replace `\"your tweet text\"` with an expression that evaluates to the string you want to post instead, like so:"
]
},
{
"cell_type": "code",
"execution_count": 42,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"undefined"
]
},
"execution_count": 42,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"twitter.statuses(\n",
" \"update\",\n",
" {\"status\": cities[0][\"city\"] + \", \" + cities[0][\"state\"]},\n",
" yourAccessToken,\n",
" yourTokenSecret,\n",
" function(error, data, response) {\n",
" if (error) {\n",
" console.log(\"something went wrong: \", error);\n",
" }\n",
" }\n",
");"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Maintaining state\n",
"\n",
"Okay. Now we know how to make a simple program that posts a tweet. How do we make a program that goes through a list of data, and makes a tweet for each item? The strategy we're going to use is this: every time you run the program, it will tweet an item from the list. The program will then increment a counter. Each time the program is subsequently run, it will check the counter and tweet the text associated with that item in the list.\n",
"\n",
"By \"counter,\" I mean some piece of data, stored *outside* of the program, that we'll update each time the program is run, in this case so that the number is one greater than its previous contents. This is a way of maintaining \"state\" (e.g., the current status of our bot) between discrete executions of the program.\n",
"\n",
"There are many ways to maintain state. We could use a database, for example, or a web service. For simplicity's sake, we'll use the simplest way of maintaining state: a file.\n",
"\n",
"### Writing a file\n",
"\n",
"In this case, we're going to make a file that contains a number in plain-text format. So first, we'll need to make a file and write some data out to it. In Node, we'll do this with the `fs.writeFileSync()` function. Here's some code that writes the string `how are you` to a file called `hello.txt`:"
]
},
{
"cell_type": "code",
"execution_count": 51,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"undefined"
]
},
"execution_count": 51,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"fs.writeFileSync(\"hello.txt\", \"how are you\", \"utf8\");"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The first parameter is the name of the file to write, the second parameter is the string to write, and the third parameter is the encoding to use (this should always be `\"utf8\"`). Check the directory where your Node program (or interactive session) is running; you should see a file called `hello.txt` with contents `how are you`.\n",
"\n",
"What we want to do is create a file called `counter.txt` and write the string `0` to it. Run the following code:"
]
},
{
"cell_type": "code",
"execution_count": 52,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"undefined"
]
},
"execution_count": 52,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"fs.writeFileSync(\"counter.txt\", \"0\", \"utf8\");"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Reading a file\n",
"\n",
"As mentioned above, you can read the contents of a file using `fs.readFileSync()`. Use the resulting object's `.toString()` method to convert the buffer to a string."
]
},
{
"cell_type": "code",
"execution_count": 53,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"'0'"
]
},
"execution_count": 53,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"var counterBuffer = fs.readFileSync(\"counter.txt\");\n",
"var counter = counterBuffer.toString();\n",
"counter"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In order to use this value as an index, we'll need to additionally convert it into an integer:"
]
},
{
"cell_type": "code",
"execution_count": 55,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"undefined"
]
},
"execution_count": 55,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"var counterInt = parseInt(counter)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"And now, use the value to access a particular entry in the `cities` array:"
]
},
{
"cell_type": "code",
"execution_count": 58,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"'New York'"
]
},
"execution_count": 58,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"cities[counterInt][\"city\"]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Incrementing the value\n",
"\n",
"To increment the value, we'll perform two steps:\n",
"\n",
"* Increment the number itself using `++`;\n",
"* Convert the number to a string using `.toString()`;\n",
"* Write the string back to `counter.txt` using `fs.writeFileSync()`.\n",
"\n",
"Here's what it looks like:"
]
},
{
"cell_type": "code",
"execution_count": 59,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"undefined"
]
},
"execution_count": 59,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"counterInt++;\n",
"var counterString = counterInt.toString();\n",
"fs.writeFileSync(\"counter.txt\", counterString, \"utf8\");"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If you examine `counter.txt` now, you'll see it contains the string `1`. Here's the entire round-trip process, compressed a little bit by combining expressions:"
]
},
{
"cell_type": "code",
"execution_count": 69,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"10\n"
]
},
{
"data": {
"text/plain": [
"undefined"
]
},
"execution_count": 69,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"var counter = parseInt(fs.readFileSync(\"counter.txt\").toString());\n",
"console.log(counter);\n",
"counter++;\n",
"fs.writeFileSync(\"counter.txt\", counter.toString(), \"utf8\");"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If you run the lines of code over and over again (as a standalone program, or in an interactive session), you'll see the number increment both on the screen and in `counter.txt`."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Putting it all together\n",
"\n",
"Now it's possible to write all of the code necessary to write a program that, on each subsequent run, tweets the next item from a list. Here's the whole thing, put together. Before you run this the first time, make sure there's a file called \"counter.txt\" in the same directory as the rest of your code, and that the file contains the number \"0\". You can quickly accomplish this by running the following on the command line:\n",
"\n",
" echo \"0\" >counter.txt\n",
" \n",
"Make sure to fill in the `consumerKey` etc. variables as appropriate!"
]
},
{
"cell_type": "code",
"execution_count": 73,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"undefined"
]
},
"execution_count": 73,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"var twitterAPI = require('node-twitter-api');\n",
"var util = require('util');\n",
"var fs = require('fs');\n",
"\n",
"var consumerKey = \"\";\n",
"var consumerSecret = \"\";\n",
"var accessToken = \"\";\n",
"var tokenSecret = \"\";\n",
"\n",
"var twitter = new twitterAPI({\n",
" consumerKey: consumerKey,\n",
" consumerSecret: consumerSecret});\n",
"\n",
"var citiesData = JSON.parse(fs.readFileSync(\"us_cities.json\"));\n",
"var cities = citiesData[\"cities\"];\n",
"\n",
"var counter = parseInt(fs.readFileSync(\"counter.txt\").toString());\n",
"\n",
"twitter.statuses(\"update\",\n",
" {\"status\": cities[counter].city},\n",
" accessToken,\n",
" tokenSecret,\n",
" function(error, data, response) {\n",
" if (error) {\n",
" console.log(\"something went wrong: \" + util.inspect(error));\n",
" }\n",
" }\n",
");\n",
"\n",
"counter++;\n",
"fs.writeFileSync(\"counter.txt\", counter.toString(), \"utf8\");"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[The full source code for this bot is here](https://github.com/aparrish/example-twitter-bot-node/tree/master/cities_of_the_usa). In the version of the code on Github, you'll specify your authorization tokens on the command line instead of in the code itself, like so:\n",
"\n",
" node bot.js YOUR_APP_KEY YOUR_APP_SECRET YOUR_ACCESS_TOKEN YOUR_TOKEN_SECRET\n",
" \n",
"... replacing the phrases in all caps with the corresponding values you obtained from your Twitter application page at `get_tokens.js`."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Scheduling your bot\n",
"\n",
"The fun part of bots is that they do things on their own without your having to intervene. So ideally, we'd like to be able to make our bot program run on an interval, instead of having to run the program by hand every time. To do this, we're going to use a tool called `cron`, which is a scheduling program built in to most variants of UNIX (like Linux or macOS). (If you're deploying bots on Windows, consider [Task Scheduler](https://msdn.microsoft.com/en-us/library/windows/desktop/aa383614.aspx)).\n",
"\n",
"The `cron` program is very powerful, but also very persnickety and a little bit weird. The way it works is this: you edit your \"cron table\" or \"crontab\" using the `crontab` command, using a special syntax that indicates which program to run and what times to run the program. The `cron` program, which is constantly running on your computer, notices that you've changed your crontab, and then schedules the programs to run as you've specified.\n",
"\n",
"For the purposes of the following, I'm going to assume that you're using [this version](https://github.com/aparrish/example-twitter-bot-node/tree/master/cities_of_the_usa) of the US cities bot.\n",
"\n",
"### Using crontab\n",
"\n",
"Before you schedule your bot, you need to know two things. The first is the *directory* that your code resides in. To find this out, open a Terminal window to the folder where your bot's program resides and type\n",
"\n",
" pwd\n",
" \n",
"... at the command line.\n",
"\n",
"The second thing you need to know is the *path to the node executable*. You can find this out by typing\n",
"\n",
" which node\n",
" \n",
"... at the command line. Copy and paste both of these items to a scratch text file so you remember what they are.\n",
"\n",
"The easiest way to edit your crontab is from the command line. If you've never used a text-mode editor before, then I suggest you type this first:\n",
"\n",
" export EDITOR=nano\n",
" \n",
"This line tells your computer to use the `nano` text editor when you edit your crontab. (`nano` is a relatively easy to use text editor. If you don't specify an editor with this command, your computer will use `vim` by default. `vim` is a powerful text editor, but very arcane, and not recommended for newcomers).\n",
"\n",
"After setting your editor, type:\n",
"\n",
" crontab -e\n",
" \n",
"You'll be shown the `nano` text editor. If you've never used `crontab` before, the file will be blank. Type the following line into the text editor:\n",
"\n",
" * * * * * cd BOT_DIRECTORY; PATH_TO_NODE bot.js YOUR_APP_KEY YOUR_APP_SECRET YOUR_ACCESS_TOKEN YOUR_TOKEN_SECRET >>log.txt 2>&1\n",
" \n",
"... making the following replacements:\n",
"\n",
"* Replace `BOT_DIRECTORY` with the output of the `pwd` command above\n",
"* Replace `PATH_TO_NODE` with the output of the `which node` command\n",
"* Replace `YOUR_APP_KEY`, `YOUR_APP_SECRET`, `YOUR_ACCESS_TOKEN`, `YOUR_TOKEN_SECRET` with the corresponding authorization credentials for your bot.\n",
"\n",
"All of this needs to be on the *same line*. It will probably be a long line! Make sure to be as precise as possible. One misplaced character may make it so your bot fails to work. (Note: If you've included your credentials in your bot's code itself, you don't need to put the authorization information into the crontab.)\n",
"\n",
"Save the file (in `nano`, hit Ctrl-X and then hit \"Y\"). With any luck, your bot is now running once every minute. Wait for a bit and reload your bot's timeline! You should see a new post every minute.\n",
"\n",
"### If something goes wrong\n",
"\n",
"If you've typed the command in crontab correctly, then errors should be displayed in a file called `log.txt` in the same directory as your bot. (That's what the `>>log.txt 2>&1` at the end of the crontab line is for.) You can examine this file in any text editor.\n",
"\n",
"### Slowing down the frequency\n",
"\n",
"Running your bot every minute is probably excessive. By changing some of the asterisks in the crontab line, you can change how frequently your program runs. For now, we're just going to play with the first and second asterisks, which allow you to control which hour and which minutes of the hour your bot runs.\n",
"\n",
"The second asterisk tells `cron` which hours of the day to run your program, specified with numbers from 0—23. So for example, if you wanted your bot to run every minute, but only from 2pm to 3pm on any given day, the beginning of your crontab line would look like this:\n",
"\n",
" * 14 * * *\n",
" \n",
"If you want your program to run every hour, but only at 5 minutes past the hour, your crontab would look like this:\n",
"\n",
" 5 * * * *\n",
" \n",
"You can combine the minutes and hours like so:\n",
"\n",
" 5 14 * * *\n",
" \n",
"... which would tell `cron` to only run your program at 2:05pm every day.\n",
"\n",
"You can specify more than one hour or minute in either of these fields by including a list of numbers separated by commas. For example, to run the program at 2:05pm and 6:05pm, you might write:\n",
"\n",
" 5 14,18 * * *\n",
" \n",
"(Note: no spaces before or after the comma!)\n",
"\n",
"To have your bot post six times a day at the top of the hour:\n",
"\n",
" 0 0,4,8,12,16,20 * * *\n",
" \n",
"To have your bot run at the top of the hour and half-past, every hour:\n",
"\n",
" 0,30 * * * *\n",
" \n",
"## Deploying your bot\n",
"\n",
"Everything we've set up above will work only on *your own computer*. This means that your bot will stop tweeting when you close your laptop lid! This is kind of cute but hardly ideal. In order for your bot to work all the time, you'll need to *deploy* it by copying the code to a computer that is always on and always connected to the Internet.\n",
"\n",
"There are several ways of accomplishing this! The first is simply to use a second computer, like a spare laptop you have sitting around or a Raspberry Pi. A more common way to deploy a bot is to use a *cloud server*: essentially, a computer that you're renting by the minute or month from another company. You won't ever see this computer physically, you'll just connect to it over the network.\n",
"\n",
"In class, I'll show you how to set a bot up on Digital Ocean. Here's the basic overview:\n",
"\n",
"* [Sign up for the Github Student Developer Pack](https://education.github.com/pack). This will give you free hours on Digital Ocean.\n",
"* Log into Digital Ocean and click the \"Create Droplet\" button in the upper right-hand corner.\n",
"* Under \"Choose an image,\" select the \"One click apps\" tab and select \"NodeJS 6.9.1 on 14.04.\" (Choosing an \"image\" is essentially choosing which software should be pre-installed on your cloud server. In this case, you're selecting a server that already has Node and NPM installed on a basic Linux system.)\n",
"* Under \"Choose a size,\" you can safely select the $5/mo tier.\n",
"* Skip \"Add Block Storage.\"\n",
"* Under \"Choose a datacenter region,\" pick whatever you'd like, but New York is best if you'll be working on your bots in, well, New York. (This determines the physical location of the machine that is being allocated for you.)\n",
"* Skip \"Select Additional Options.\"\n",
"* If you're familiar with SSH keys, feel free to create or add a key under \"Add your SSH keys.\" If not, skip this section. DigitalOcean will e-mail you the root password for your machine.\n",
"* Choose a hostname that makes sense to you. (You'll be able to re-use this machine for other bots, so maybe pick something like `botshop-server`.)\n",
"\n",
"Once you've received your password, log into the server with `ssh` like so (assuming you opted not to use an SSH key):\n",
"\n",
" ssh root@YOUR_DROPLET_IP_ADDRESS\n",
" \n",
"... replacing `YOUR_DROPLET_IP_ADDRESS` with the IP address in your Droplets list. You'll be asked to change your password the first time you log in.\n",
"\n",
"Afterwards, use the `mkdir` command to make a directory for your bot: \n",
"\n",
" mkdir cities\n",
"\n",
"(You can choose a name other than `cities`, of course.)\n",
" \n",
"To copy your bot code to the server, you can either use [SFTP](https://www.digitalocean.com/community/tutorials/how-to-use-sftp-to-securely-transfer-files-with-a-remote-server) or `scp`. When you're copying the files, make sure to include `package.json`. You don't need to copy the `node_modules` directory, if present. To use `scp` to copy your files, open a Terminal session, change your working directory to the directory with your bot's code, and type:\n",
"\n",
" scp * root@YOUR_DROPLET_IP_ADDRESS:~/cities\n",
" \n",
"... replacing `YOUR_DROPLET_IP_ADDRESS` with your droplet's IP address, and `cities` with the name of the directory that you created above.\n",
"\n",
"To run your bot, log back into the server, switch to the directory you made before (`cd cities`) and install the needed packages from `npm` by typing `npm install` in the directory with your bot code. Then, using the instructions in the previous section, add your bot's code to cron. And voila!\n",
"\n",
"## Further reading\n",
"\n",
"* [Great tutorial on setting up a NodeJS app on Digital Ocean](https://scotch.io/tutorials/how-to-host-a-node-js-app-on-digital-ocean). For our purposes, follow this tutorial up to \"Hosting the Node.JS app\".\n",
"* [Good overview of using cron on OSX](https://ole.michelsen.dk/blog/schedule-jobs-with-crontab-on-mac-osx.html)\n",
"* [Node on Raspberry Pi (for bots)](http://www.jeffreythompson.org/blog/2015/11/20/tutorial-node-on-raspberry-pi-for-bots/)\n",
"* If you'd prefer to work in Python, see my [everywordbot](https://github.com/aparrish/everywordbot)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Javascript (Node.js)",
"language": "javascript",
"name": "javascript"
},
"language_info": {
"file_extension": ".js",
"mimetype": "application/javascript",
"name": "javascript",
"version": "4.2.2"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment