class: middle, center, inverse # Make a Twitter bot with {rtweet} and GitHub Actions #
LondonR, 2022-02-17<br> .small-font[Updated 2022-07-22] Matt Dray .small-font[[matt-dray.github.io/mapbot-londonr](https://matt-dray.github.io/mapbot-londonr)] ??? * Find the slides online now * See presenter notes by pressing the <kbd>P</kbd> key --- class: middle # tl;dr .pull-left[ * [.dark-font[Visit @londonmapbot .super[
]]](https://www.twitter.com/londonmapbot) * [.dark-font[Fork on GitHub .super[
]]](https://www.github.com/matt-dray/londonmapbot) * [.dark-font[Read the original blog .super[
]]](https://www.rostrum.blog/2020/09/21/londonmapbot/) ] .pull-right[ <img src='img/qr.png' alt='QR code pointing to the source for these slides on GitHub.'> ] ??? * Too long; didn't read * You can go to sleep now --- class: middle # Eh? 1. Who? 1. What? 1. How? ??? * Who am I * What I'm talking about * How to do it yourself --- class: middle, center, inverse # Who? --- class: middle, center <a href="https://www.matt-dray.com"><img src="img/mattdray-avatar.png", width = 40%></a> .small-font[
[.dark-font[matt-dray.com]](https://www.matt-dray.com)
[.dark-font[@mattdray]](https://www.twitter.com/mattdray)
[.dark-font[matt-dray]](https://www.github.com/matt-dray)
[.dark-font[rostrum.blog]](https://www.rostrum.blog)] ??? * Data analyst in the public sector * Everything here is my thoughts * I do R for fun --- background-image: url("img/rostrum-blog-home.png") background-size: 100% ??? * I write for fun about (1) R solutions, (2) zeitgeists, (3) stretching R too far (3) mild trolling --- class: middle, center, inverse # What? ??? * This talk is about one showerthought in particular * There's three components to talk about (1) {rtweet}, (2) Mapbox, (3) GitHub Actions --- class: middle, center background-image: url("img/twitter-londonmapbot-profile.png") background-size: 100% ??? * The @londonmapbot Twitter profile --- class: middle, center background-image: url("img/twitter-londonmapbot-tweet.png") background-size: 100% --- background-image: url("img/mapbox-twickenham-Eu0ssieXMAAuSmw.jpg") background-size: 120% --- background-image: url("img/mapbox-buckingham-palace-Ewv-OO3VkAcDV8k.jpg") background-size: 120% --- background-image: url("img/mapbox-heathrow-E9DEbx9XMAIGD4c.jpg") background-size: 120% --- background-image: url("img/mapbox-boat-E3eIMQvUYAA-u-t.jpg") background-size: 120% --- background-image: url("img/mapbox-roads-FHxwDeDVcAUUb69.jpg") background-size: 120% --- class: middle # [.dark-font[
{rtweet} .super[
]]](https://docs.ropensci.org/rtweet/) * Access Twitter from R * Free API, with limitations ??? * R package by Mike Kearney * Limited to 18k tweets every 15 mins * To request more than that, set `retryonratelimit = TRUE` --- class: middle, center background-image: url("img/rtweet-homepage.png") background-size: 100% --- class: middle # [.dark-font[
Mapbox .super[
]]](https://www.mapbox.com/) * [.dark-font[Satellite imagery and maps .super[
]]](https://docs.mapbox.com/api/overview/) * [.dark-font[Free API, with limitations .super[
]]](https://docs.mapbox.com/api/maps/static-images/#static-images-api-restrictions-and-limits) ??? * 1250 requests per minute to the static image API --- class: middle, center background-image: url("img/mapbox-playground.png") background-size: 100% --- class: middle # [.dark-font[
GitHub Actions .super[
]]](https://github.com/features/actions) * Run code on schedule * [.dark-font[Free, with limitations .super[
]]](https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits) ??? * Courtesy of GitHub, we can switch on a remote computer on push or schedule, run some code, then go to sleep * Used for continuous integration, like R package testing, coverage and {pkgdown} deployment --- class: middle, center background-image: url("img/github-actions-dehex.png") background-size: 100% ??? * Here is the Actions pane in a GiutHub repo for one of my R packages, {dehex} * There's three Actions: (1) R-CMD check, (2) rebuild the {pkgdown} website, (3) assess test coverage * These can be reported automatically in badges in the repo README --- class: middle, center, inverse # How? ??? * Now we have the components, what are the steps to building the bot? * I won't go into minute detail, but the steps are linked to the appropriate guidance pages so you can try it out after the session --- class: middle # .dark-font[
Twitter] 1. .dark-font[Start an account] 1. [.dark-font[Get developer status .super[
]]](https://developer.twitter.com/en/docs/twitter-api/getting-started/getting-access-to-the-twitter-api) 1. [.dark-font[Get elevated API access .super[
]]](https://developer.twitter.com/en/docs/twitter-api/getting-started/about-twitter-api) 1. [.dark-font[Create 'app', get keys .super[
]]](https://developer.twitter.com/en/docs/apps/overview) ??? * I did this with a fresh email account and * 'Elevated' status allows access to API v1.1, which is what {rtweet} uses * [.dark-font[It was Oscar Baruffa who told me about this .super[
]]](https://oscarbaruffa.com/twitterbot/) --- class: middle #
Mapbox 1. [.dark-font[Sign up .super[
]]](https://account.mapbox.com/auth/signup/) 1. [.dark-font[Get tokens .super[
]]](https://docs.mapbox.com/api/accounts/tokens/) --- class: middle #
Github 1. Create a public repo 1. [.dark-font[Store keys/tokens as secrets .super[
]]](https://docs.github.com/en/actions/security-guides/encrypted-secrets) 1. Create two scripts<br> - .github/workflows/bot.yml - bot.R ??? * Sharing is caring * A public repo is less constrained by Actions limits * Secrets can be added in the repo Settings > Secrets (left-panel) > Actions > Repository secrets * Secrets in the londonmapbot repopp: MAPBOX_PUBLIC_ACCESS_TOKEN, TWITTER_ACCESS_TOKEN, TWITTER_ACCESS_TOKEN_SECRET, TWITTER_CONSUMER_API_KEY, TWITTER_CONSUMER_API_SECRET * Just two scripts are required, and in theory only one * A YAML file is just a specially formatted text document with some instructions that GitHub Actions will parse and execute * I've put the R code into a simple R script that is called by the YAML file --- class: middle # [.dark-font[bot.yml .super[
]]](https://github.com/matt-dray/londonmapbot/blob/master/.github/workflows/londonmapbot.yml) ```r name: londonmapbot on: schedule: - cron: '42 0/2 * * *' jobs: londonmapbot-post: runs-on: macOS-latest env: TWITTER_CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }} TWITTER_CONSUMER_API_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET }} TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} MAPBOX_PUBLIC_ACCESS_TOKEN: ${{ secrets.MAPBOX_PUBLIC_ACCESS_TOKEN }} steps: - uses: actions/checkout@v2 - uses: r-lib/actions/setup-r@master - name: Install rtweet package run: Rscript -e 'install.packages("rtweet", dependencies = TRUE)' - name: Create and post tweet run: Rscript londonmapbot-tweet.R ``` --- class: middle # [.dark-font[bot.yml .super[
]]](https://github.com/matt-dray/londonmapbot/blob/master/.github/workflows/londonmapbot.yml) ```r name: londonmapbot on: schedule: * - cron: '42 0/2 * * *' jobs: londonmapbot-post: runs-on: macOS-latest env: TWITTER_CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }} TWITTER_CONSUMER_API_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET }} TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} MAPBOX_PUBLIC_ACCESS_TOKEN: ${{ secrets.MAPBOX_PUBLIC_ACCESS_TOKEN }} steps: - uses: actions/checkout@v2 - uses: r-lib/actions/setup-r@master - name: Install rtweet package run: Rscript -e 'install.packages("rtweet", dependencies = TRUE)' - name: Create and post tweet run: Rscript londonmapbot-tweet.R ``` ??? * Cron here reads as 'minute 42 every two hours from hour 0' * Use a cron helper like [.dark-font[crontab.guru .super[
]]](https://crontab.guru/), or my own [.dark-font[{dialga} package .super[
]]](https://www.rostrum.blog/2021/04/10/dialga/) for converting R to cron to English --- class: middle # [.dark-font[bot.yml .super[
]]](https://github.com/matt-dray/londonmapbot/blob/master/.github/workflows/londonmapbot.yml) ```r name: londonmapbot on: schedule: - cron: '42 0/2 * * *' jobs: londonmapbot-post: * runs-on: macOS-latest env: TWITTER_CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }} TWITTER_CONSUMER_API_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET }} TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} MAPBOX_PUBLIC_ACCESS_TOKEN: ${{ secrets.MAPBOX_PUBLIC_ACCESS_TOKEN }} steps: - uses: actions/checkout@v2 - uses: r-lib/actions/setup-r@master - name: Install rtweet package run: Rscript -e 'install.packages("rtweet", dependencies = TRUE)' - name: Create and post tweet run: Rscript londonmapbot-tweet.R ``` --- class: middle # [.dark-font[bot.yml .super[
]]](https://github.com/matt-dray/londonmapbot/blob/master/.github/workflows/londonmapbot.yml) ```r name: londonmapbot on: schedule: - cron: '42 0/2 * * *' jobs: londonmapbot-post: runs-on: macOS-latest env: * TWITTER_CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }} TWITTER_CONSUMER_API_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET }} TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} MAPBOX_PUBLIC_ACCESS_TOKEN: ${{ secrets.MAPBOX_PUBLIC_ACCESS_TOKEN }} steps: - uses: actions/checkout@v2 - uses: r-lib/actions/setup-r@master - name: Install rtweet package run: Rscript -e 'install.packages("rtweet", dependencies = TRUE)' - name: Create and post tweet run: Rscript londonmapbot-tweet.R ``` ??? * The 'env' step is adding the secrets to the environment from the secrets stash in the repo --- class: middle # [.dark-font[bot.yml .super[
]]](https://github.com/matt-dray/londonmapbot/blob/master/.github/workflows/londonmapbot.yml) ```r name: londonmapbot on: schedule: - cron: '42 0/2 * * *' jobs: londonmapbot-post: runs-on: macOS-latest env: TWITTER_CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }} TWITTER_CONSUMER_API_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET }} TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} MAPBOX_PUBLIC_ACCESS_TOKEN: ${{ secrets.MAPBOX_PUBLIC_ACCESS_TOKEN }} steps: - uses: actions/checkout@v2 * - uses: r-lib/actions/setup-r@master - name: Install rtweet package run: Rscript -e 'install.packages("rtweet", dependencies = TRUE)' - name: Create and post tweet run: Rscript londonmapbot-tweet.R ``` ??? * You can handy pre-prepared code from r-lib to set up the R environment on the remote worker --- class: middle # [.dark-font[bot.yml .super[
]]](https://github.com/matt-dray/londonmapbot/blob/master/.github/workflows/londonmapbot.yml) ```r name: londonmapbot on: schedule: - cron: '42 0/2 * * *' jobs: londonmapbot-post: runs-on: macOS-latest env: TWITTER_CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }} TWITTER_CONSUMER_API_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET }} TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} MAPBOX_PUBLIC_ACCESS_TOKEN: ${{ secrets.MAPBOX_PUBLIC_ACCESS_TOKEN }} steps: - uses: actions/checkout@v2 - uses: r-lib/actions/setup-r@master - name: Install rtweet package run: Rscript -e 'install.packages("rtweet", dependencies = TRUE)' - name: Create and post tweet * run: Rscript londonmapbot-tweet.R ``` ??? * You can split your steps up and name them, so you'll know where any errors occurred when you read your GitHub Actions logs * You can write R directly into this file, or source an R script, like our bot.R file --- class: middle # [.dark-font[bot.R (1) .super[
]]](https://github.com/matt-dray/londonmapbot/blob/master/londonmapbot-tweet.R) ```r *londonmapbot_token <- rtweet::rtweet_bot( api_key = Sys.getenv("TWITTER_CONSUMER_API_KEY"), api_secret = Sys.getenv("TWITTER_CONSUMER_API_SECRET"), access_token = Sys.getenv("TWITTER_ACCESS_TOKEN"), access_secret = Sys.getenv("TWITTER_ACCESS_TOKEN_SECRET") ) lon <- round(runif(1, -0.489, 0.236), 4) lon <- format(lon, scientific = FALSE) lat <- round(runif(1, 51.28, 51.686), 4) img_url <- paste0( "https://api.mapbox.com/styles/v1/mapbox/satellite-v9/static/", paste0(lon, ",", lat), ",15,0/600x400@2x?access_token=", Sys.getenv("MAPBOX_PUBLIC_ACCESS_TOKEN") ) temp_file <- tempfile(fileext = ".jpeg") download.file(img_url, temp_file) ``` --- class: middle # [.dark-font[bot.R (1) .super[
]]](https://github.com/matt-dray/londonmapbot/blob/master/londonmapbot-tweet.R) ```r londonmapbot_token <- rtweet::rtweet_bot( api_key = Sys.getenv("TWITTER_CONSUMER_API_KEY"), api_secret = Sys.getenv("TWITTER_CONSUMER_API_SECRET"), access_token = Sys.getenv("TWITTER_ACCESS_TOKEN"), access_secret = Sys.getenv("TWITTER_ACCESS_TOKEN_SECRET") ) lon <- round(runif(1, -0.489, 0.236), 4) lon <- format(lon, scientific = FALSE) lat <- round(runif(1, 51.28, 51.686), 4) img_url <- paste0( "https://api.mapbox.com/styles/v1/mapbox/satellite-v9/static/", * paste0(lon, ",", lat), ",15,0/600x400@2x?access_token=", Sys.getenv("MAPBOX_PUBLIC_ACCESS_TOKEN") ) temp_file <- tempfile(fileext = ".jpeg") download.file(img_url, temp_file) ``` ??? * Sample latitude and longitude * Put these into the Mapbox API string * Download file to temporary location * bot.yml called the secrets from the repo into the environment, now we can pull the tokens into an R object * The token can then be used to access the Twitter API * Updated to reflect changes in {rtweet} v1.0 --- class: middle # [.dark-font[bot.R (2) .super[
]]](https://github.com/matt-dray/londonmapbot/blob/master/londonmapbot-tweet.R) ```r latlon_details <- paste0( lat, ", ", lon, "\n", "https://www.openstreetmap.org/#map=17/", lat, "/", lon, "/" ) alt_text <- paste( "A satellite image of a random location in Greater London,", "provided by MapBox. Typically contains a residential or", "industrial area, some fields or a golf course." ) *rtweet::post_tweet( status = latlon_details, media = temp_file, media_alt_text = alt_text, token = londonmapbot_token ) ``` ??? * Generate a string containing the coordinates and an OpenStreetMap link * Post the tweet, containing the text string, image and alt text, using the API token * Updated to reflect changes in {rtweet} v1.0 --- class: middle, center, inverse # [The mapbotverse .super[
]](https://twitter.com/i/lists/1492559073287581707) ??? * You don't need to take my word for it * Many other people have adapted the code for other map bots and made it much better * But also people have created scheduled bots that don't involve maps * Hopefully this is inspirational --- background-image: url("img/twitter-mapbotverse.png") background-size: 100% ??? * To provide ideas, I've added to a Twitter list the accounts I can find that have used londonmapbot code, improved it, or were inspired by it * I'll give three different examples: two mapbots, one that does something else --- background-image: url("img/twitter-mapbot-esmapbot.png") background-size: 100% ??? * @esmapbot by Roberto Jiménez (@roberer_) * Samples a point within a geojson of Spain (rather than a simple bounding box) * Uses Google Maps instead of OpenStreetMap * Emojis --- background-image: url("img/twitter-mapbot-narrowbotr.png") background-size: 100% ??? * @narrowbotR by Matt Kerlogue (@mattkerlogue) * Selects a random location on the English and Welsh canal network * Fetches a Flickr image from nearby, applying a 'photo score' to fetch the best candidate image * Otherwise fetches a Mapbox image --- background-image: url("img/twitter-mapbot-bigbookofr.png") background-size: 100% ??? * @BigBookofR by Oscar Baruffa (@OscarBaruffa) * Links to a random resource from the Big Book of R * It was Oscar who told me about needing 'elevated' developer status to access the old, version 1.1, Twitter APi, which is what {rtweet} was designed to communicate with --- background-image: url("img/github-londonmapbot.png") background-size: 100% ??? * You can do it too * Approach 1: start fresh * Approach 2: fork the repo, make changes * Approach 3: use the repo as a template, make changes * Approach 4: fork one of the other map bots that have used more complex approaches * Let me know about it! --- class: middle, center, inverse # Make a Twitter bot with {rtweet} and GitHub Actions #
[Visit @londonmapbot .super[
]](https://www.twitter.com/londonmapbot)<br> [Fork on GitHub .super[
]](https://www.github.com/matt-dray/londonmapbot)<br> [Read the original blog .super[
]](https://www.rostrum.blog/2020/09/21/londonmapbot/)<br> .small-font[
[matt-dray.com](https://www.matt-dray.com)
[@mattdray](https://www.twitter.com/mattdray)
[matt-dray](https://www.github.com/matt-dray)
[rostrum.blog](https://www.rostrum.blog)]