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.
The components
The source code is quite simple. There’s two files, basically:
- a single YAML file containing the action2
- a single R script that generates the tweet and posts it
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.3 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, to run every hour, once a day, or whatever.
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 hour4
- 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:
- use some pre-written code by GitHub to check out the repo
- use some prewritten code from r-lib that sets up R
- install the {rtweet} package and its dependencies
- run the named R script from the repo
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.
Update
Twitter has moved to version 2.0 of their API since this post was written. As things stand in February 2022 (hello from the future), you will need to ask for ‘elevated’ access in Twitter’s Developer Portal to ensure you can reach version 1.1 of the API, which is what {rtweet} is set up to communicate with.
Huge thanks to Oscar Baruffa, who learnt about this hard way when setting up a Twitter bot for the excellent Big Book of R (an index of 250+ free books for R programming).
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::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")
)
# 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.")
Update
{rtweet} version 1.0 was released with breaking changes in July 2022 and so I’ve changed the code above to use the function rtweet_bot()
instead of create_token()
. You can read a separate blogpost about these changes.
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 appears to be a generous free allowance of 1250 requests per minute for the static image API.
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:
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(fileext = ".jpeg")
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.
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:
- take a look at the @londonmapbot profile
- find the source on GitHub
- inspect the YAML file that runs the action
- see the R script that generates and posts the image
The GitHub README also lists a few other map bots—which I’ve christened the ‘mapbotverse’—that have taken inspiration from londonmapbot; take a look at those too.
Of course, you should fork the repo, or use it as a template, to create your own bot. Let me know what you get up to.
Do give me suggestions and pull requests, or tell me how good you are at identifying the granular location in each image.
Session info
## ─ Session info ───────────────────────────────────────────────────────────────
## setting value
## version R version 4.2.0 (2022-04-22)
## os macOS Big Sur/Monterey 10.16
## system x86_64, darwin17.0
## ui X11
## language (EN)
## collate en_GB.UTF-8
## ctype en_GB.UTF-8
## tz Europe/London
## date 2022-07-23
## pandoc 2.17.1.1 @ /Applications/RStudio.app/Contents/MacOS/quarto/bin/ (via rmarkdown)
##
## ─ Packages ───────────────────────────────────────────────────────────────────
## package * version date (UTC) lib source
## blogdown 1.9 2022-03-28 [1] CRAN (R 4.2.0)
## bookdown 0.26 2022-04-15 [1] CRAN (R 4.2.0)
## bslib 0.3.1 2021-10-06 [1] CRAN (R 4.2.0)
## cli 3.3.0 2022-04-25 [1] CRAN (R 4.2.0)
## digest 0.6.29 2021-12-01 [1] CRAN (R 4.2.0)
## evaluate 0.15 2022-02-18 [1] CRAN (R 4.2.0)
## fastmap 1.1.0 2021-01-25 [1] CRAN (R 4.2.0)
## fontawesome 0.2.2 2021-07-02 [1] CRAN (R 4.2.0)
## htmltools 0.5.2 2021-08-25 [1] CRAN (R 4.2.0)
## jquerylib 0.1.4 2021-04-26 [1] CRAN (R 4.2.0)
## jsonlite 1.8.0 2022-02-22 [1] CRAN (R 4.2.0)
## knitr 1.39 2022-04-26 [1] CRAN (R 4.2.0)
## magrittr 2.0.3 2022-03-30 [1] CRAN (R 4.2.0)
## R6 2.5.1 2021-08-19 [1] CRAN (R 4.2.0)
## rlang 1.0.2 2022-03-04 [1] CRAN (R 4.2.0)
## rmarkdown 2.14 2022-04-25 [1] CRAN (R 4.2.0)
## rstudioapi 0.13 2020-11-12 [1] CRAN (R 4.2.0)
## sass 0.4.1 2022-03-23 [1] CRAN (R 4.2.0)
## sessioninfo 1.2.2 2021-12-06 [1] CRAN (R 4.2.0)
## stringi 1.7.6 2021-11-29 [1] CRAN (R 4.2.0)
## stringr 1.4.0 2019-02-10 [1] CRAN (R 4.2.0)
## xfun 0.30 2022-03-02 [1] CRAN (R 4.2.0)
## yaml 2.3.5 2022-02-21 [1] CRAN (R 4.2.0)
##
## [1] /Library/Frameworks/R.framework/Versions/4.2/Resources/library
##
## ──────────────────────────────────────────────────────────────────────────────
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.↩︎
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 note that there are already actions on the GitHub Marketplace built specifically 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.↩︎
There’s a number of sites that can help you build a cron string. I built a toy package, {dialga}, to help convert from R to cron to English. I may change the specific posting frequency for londonmapbot in future.↩︎