A Twitter bot with {rtweet} and GitHub Actions

The Twitter profile page for londonmapbot.

@londonmapbot on Twitter.

tl;dr

I made @londonmapbot: a simple Twitter bot that uses the R package {rtweet}, GitHub Actions and the Mapbox API. Find the source on Github.

London from (socially-distant) space

I’ve wanted to make a Twitter bot for a while, but it seemed like Hard Work. Spoiler: it’s not.

So, I’ve made @londonmapbot: a completely unsophisticated proof-of-concept Twitter bot.

What does it do? It posts a satellite image from random coordinates in Greater London (well, from a bounding box roughly within the M25 motorway) every half hour. Below is an example image from an existing @londonmapbot tweet. Can you guess where it is?1

A satellite image of part of London, showing what looks like a large number of tennis courts, including some in stadia.

The code for this runs remotely. You can set it up, let it run and never think about it again.

So how does it work? A scheduled GitHub Action runs R code to generate random latitude and longitude values, which are sent to the Mapbox API to retrieve a satellite picture. The image is then posted via the Twitter API that is accessed using the {rtweet} package. A link to the coordinates on OpenStreetMap is also included so you can find out exactly where the image is form.

The main purpose was to learn more about GitHub Actions, building on my previous posts about using actions for continuous integration, but I think incidentally that the tweets are quite pleasing to look at and to guess where they are.

The components

The source code is quite simple. There’s two files, basically:

Let’s look at the GitHub Actions code in the YAML file and the use of {rtweet} and Mapbox in the R file.

GitHub Actions

GitHub Actions is a platform for automating workflows remotely. In short, you write a small YAML file in the .github/workflows/ subfolder of your repo, which contains instructions for the code you want to run and when to run it. I’ve written before about using GitHub Actions for continuous integration of R packages, for example.

An action can be triggered by an event, like a git push to your repo. You can also schedule it with a cron job. I took advantage of this for londonmapbot, which executes every half-hour on :00 and :30 past each hour.

Here’s what the YAML file looks like for the londonmapbot action:

name: londonmapbot

on:
  schedule:
    - cron: '0,30 * * * *'

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

It’s interpreted like so:

  • this action is called ‘londonmapbot’
  • run this code at :00 and :30 past each hour2
  • the first (and only) job in this action is called londonmapbot-post
  • start up a remote machine with the latest macOS operating system installed (this is where your code will be run)
  • set some environmental variables, in this case keys that will be used to access the Twitter and Mapbox APIs (see the ‘Secrets’ section later in this post)
  • the steps of the job are to:

I would recommend changing your GitHub notification alerts once the bot is up and running, otherwise you’ll get a message every time the action executes. You can change this under Settings > Notifications > GitHub Actions, where you can uncheck the boxes under ‘Notifications for workflow runs on repositories set up with GitHub Actions’.

{rtweet}

The action runs an R script that generates content for a tweet and then posts it. This script makes use of the package {rtweet} by Mike Kearney, which lets you interact with the Twitter API with R functions.

You need a Twitter account, of course, and also to sign up as a Twitter developer to access the API. As a developer, you can create ‘apps’ to obtain keys: private alphanumeric passcodes that grant you access to the API.

Typically, when working locally, you would either provide these keys as bare strings, or put them in your .Renviron file. With the latter, you can then use Sys.getenv() to call them from your .Renviron, which stops you exposing the raw keys in your code.

Below is an example of how you can use {rtweet} to post a tweet from R if you’ve added the keys to your .Renviron.

# Install the package from CRAN
install.packages("rtweet")

# Create a token containing your Twitter keys
rtweet::create_token(
  app = "londonmapbot",  # the name of the Twitter app
  consumer_key = Sys.getenv("TWITTER_CONSUMER_API_KEY"),
  consumer_secret = Sys.getenv("TWITTER_CONSUMER_API_SECRET"),
  access_token = Sys.getenv("TWITTER_ACCESS_TOKEN"),
  access_secret = Sys.getenv("TWITTER_ACCESS_TOKEN_SECRET")
)

# Example: post a tweet via the API
# The keys will are in your environment thanks to create_token()
rtweet::post_tweet(status = "This is a test tweet.")

This is basically what happens in the londonmapbot R script too. When writing an action, the keys aren’t fetched from your .Renviron file, however. Instead, you can encrypt them on GitHub and provide them in the env call of your action’s YAML file. See the ‘Secrets’ section below for more detail on this.

Mapbox

Mapbox is a company with services for mapping, geocoding and navigation, which developers can use for integrating into their apps for things like asset tracking, route optimisation or anything that requires a map interface for users.

Again, you’ll need to set up a Mapbox account to get a key for using the API. While the target audience is largely commercial, there’s a generous allowance of 50,000 free requests for static satellite images.

You can then pass parameters to the Mapbox API via a URL. This is well explained in the Mapbox Documentation, which has an excellent ‘playground’ interface for you to test out your call.

You basically modify a particular URL string to ask the API for what you want. For example, you can ask for a 300x200 pixel satellite image of the coordinates of -0.1709 and 51.5065 with zoom level 12, which is Hyde Park:

https://api.mapbox.com/styles/v1/mapbox/satellite-v9/static/-0.1709,51.5065,12,0/300x200?access_token=YOUR_MAPBOX_ACCESS_TOKEN

Visiting the URL in your browser returns the requested image as a JPEG:

A satellite view of Hyde Park in London, with watermarks of copyrights for Mapbox, OpenStreetMap and Maxar.

The Serpentine is so aptly named.

Of course, you’ll need to replace the access-token placeholder (YOUR_MAPBOX_ACCESS_TOKEN) in that URL with your own Mapbox key. Rather than provide this as a bare string, the londonmapbot R script calls it from the environment (like we saw in the {rtweet} code in the last section).

Here’s the code used by londonmapbot to fetch the satellite image from Mapbox:

# Generate random coordinates
lon <- round(runif(1, -0.5, 0.27), 4)
lat <- round(runif(1, 51.3, 51.7), 4)

# Build URL and fetch from Mapbox API
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")
)

# Download the image to a temporary location
temp_file <- tempfile()
download.file(img_url, temp_file)

The code shows a paste0() statement that builds the URL with random latitude and longitude and the Mapbox key. The image from that URL is then downloaded into a temporary file, where it can be supplied to the media argument of rtweet::create_tweet() for posting to Twitter.

Secrets

I’ve mentioned in this post about keeping your keys secure. You don’t want others to copy and use your keys nefariously, so it’s a good idea not to simply paste them into your code as bare strings for the world to see.

Github lets you store secrets securely in the ‘Secrets’ section of the ‘Settings’ tab in your repo. No-one can see these, but they can be called into your code when it runs.

The GitHub website opened in the Secrets section of the Settings menu, with an example Mapbox token name being shown.

Keep it secret… keep it safe.

Let’s use the londonmapbot Twitter consumer API key as an example. First, I saved the string as a GitHub secret with the name TWITTER_CONSUMER_API_KEY. I then called this in the env section of my YAML file in the form ${{ secrets.TWITTER_CONSUMER_API_KEY }}. Running the action results in the string being pulled from the secrets stash and decrypted, where it’s available in the environment. Then the R code can call it with Sys.getenv() when access to the API is needed.

It does the job

So, you can:

Unfortunately, it doesn’t post successfully 100% of the time. You can see why by checking the actions log, which is accessed from the ‘Actions’ tab on the repo. The failed ones, with red crosses, seem mostly due to an ‘empty reply from server’. I’m okay with this for now.

In the first iteration of the action I passed the R code as a single line in the YAML file, which is suboptimal. I later tidied the code into a separate R script and declared the secrets in the YAML file. I looked at actions by Matt Kerlogue and David Keyes to do this. David’s repo is interesting from a Twitter perspective because it automates tweets provided via a Google Sheet.

I should also note that there are already actions on the GitHub Marketplace for tweeting, but they didn’t quite do what I wanted. I also wanted to write the juicy bit with R code, which I’m most familiar with.

Do give me suggestions and pull requests, or tell me how good you are at identifying the granular location in each image.


  1. Those look suspiciously like a large number of tennis courts, including some in stadia. Where could that be? The coordinates are 51.4317, -0.2151.↩︎

  2. There’s a number of sites that can help you build a cron string.↩︎