Switching the narrowbotr to Mastodon

Two years and one month ago to the day I blogged about setting up the narrowbotr, a Twitter bot that randomly posts locations on the inland waterways network in England & Wales managed by Canal & River Trust. What with everything going on with Twitter at the moment I’ve set myself up on Mastodon, where I’m @mattkerlogue@fosstodon.org, and so why not also migrate the narrowbotr.

Setting up an account

There’s more than enough posts and whatnot out there about moving to Mastodon/the fediverse, Danielle Navarro’s blog is particularly good. Long story short, there are a lot of different Mastodon instances out there, with slightly different focus and audience.

Unsurprisingly, different instances have different rules about bot accounts. Thankfully, there is a mastodon instance that specifically caters for bots: botsin.space. It has a restricted sign-up process, which means when you register you need to tell them a little about why you want to join the instance. The narrowbotr’s account was approved pretty quickly.

Chillin' out, maxin', rtootin' all cool

The original twitter bot relies on the {rtweet} package to interact with the Twitter API. But what about Mastodon, thankfully in the last couple of weeks David Schoch has put together the {rtoot} package to provide a similar method of interacting with the Mastodon API to {rtweet}.

All that was required was to set up authentication for the bot and then add the following chunk to the bot script.

  status = status_msg,
  media = tmp_file,
  alt_text = alt_msg,
  token = toot_token

Simples, well yeah, except for the authentication part.

Authenticating the bot

It’s actually not that complicated sorting authentication for the bot. However, I’ve deviated from the default approach implemented in {rtoot}1.

The default approach in {rtoot} is to generate an authentication token for your account which is then saved in an RDS file hidden away from you in the directory generated by a call to tools::R_user_dir("rtoot", "config") which on my mac is ~/Library/Preferences/org.R-project.R/R/rtoot".

While this works on my local machine, this isn’t suitable for working with a remote continuous integration system like GitHub actions. Especially as the narrowbotr is a public repo, so I can’t save the RDS file into the repository unless I want to give other people the ability to post as the narrowbotr2.

The approach for {rtweet} is to make use of environment variables. These are stored in a project level .Renviron on my local machine so are loaded when I open the project locally. On GitHub actions they are stored as secrets and the workflow file assigns them as environment variables which R can then use.

twitter_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")

So I wrote a function that mimics this approach but to work with the {rtoot} functions.

mastodon_token <- function(access_token = NULL, type = "user", 
                           instance = "botsin.space") {
  if (is.null(access_token)){
    access_token <- Sys.getenv("MASTODON_TOKEN")
  if (access_token == "") {
    stop("No Mastodon access token found")
  } else if (typeof(access_token) != "character") {
    stop("access_token must be character vector")
  } else if (length(access_token) != 1) {
    stop("access_token must be of length 1")
  } else if (nchar(access_token) != 43) {
    stop("access_token must be exactly 43 characters")
  if (type != "user") {
    stop("type must be \"user\"")
  if (is.null(instance)) {
    instance <- Sys.getenv("MASTODON_INST")
  if (instance == "") {
    stop("No Mastodon instance found")
  } else if (typeof(instance) != "character") {
    stop("instance must be character vector")
  } else if (length(instance) != 1) {
    stop("instance must be of length 1")
  token <- structure(
      bearer = access_token,
      type = type,
      instance = instance
    class = "rtoot_bearer"

First there are some checks to verify that the arguments are what we expect, and if null, calls a stored environment variable. Then the token is created, it is a simple list object that contains the token, the type of access and the instance. It also has a specific class, which we need to assign to the list in order for the other {rtoot} functions to accept it.

Then in the top of the bot script we source this function, and create a toot_token that can be used by rtoot::post_toot().


toot_token <- mastodon_token(
  access_token = Sys.getenv("MASTODON_TOKEN"),
  type = "user",
  instance = "botsin.space"

One additional variation I’ve made to the default {rtoot} authentication process has been to generate my own application API keys, which is really easy and I think has the advantage of letting you have full control of how you access the API and the permission you want to give your API token(s).

In your account settings go to the Development section (https://my.instance/settings/applications) and then select the “New application” button.

Screenshot of the settings page for the Mastodon API. The new application button is in the top right of the page. The page also shows a link to access the settings of the existing narrowbotR automation app

Mastodon API settings page

You application needs a name, and then you need to specify the scopes (i.e. the permissions) you want to give it, I’ve gone for read and write3. Just click submit to generate your API keys.

Screenshot of the page to register a new application with the Mastodon API.

New application page

Once you’ve set up your app you’ll be directed to the app’s specific settings page where you’ll be able to see the keys for this app, you’ll have a client key, a client secret and an access token.

Screenshot of the settings page for your new Mastodon API app showing the client key, client secret and access token

Mastodon API keys

As far as I can tell you only need the access token for {rtoot}, so copy this into an .Renviron file in your local project:


And add it as a secret to your GitHub Action, remembering also to edit your GitHub Actions workflow file to set it as an environment variable.

Et voilà the bot will now post simultaneously to Twitter and Mastodon.

  1. I’m going to raise an issue and maybe submit a PR to support this approach within the core package. ↩︎

  2. Surprise! I don’t! ↩︎

  3. Technically you should be as tight with your scopes as possible. In theory the bot only needs to write, and within that I think it only really needs to have two specific scopes write:media (to upload photos) and write:statuses (to make posts). ↩︎