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.
rtoot::post_toot(
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(
list(
bearer = access_token,
type = type,
instance = instance
),
class = "rtoot_bearer"
)
return(token)
}
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()
.
source("R/mastodon_token.R")
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.
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 write
3. Just
click submit to generate your API keys.
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.
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:
MASTODON_TOKEN="yoUrM4sToD0nt0k3N"
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.
-
I’m going to raise an issue and maybe submit a PR to support this approach within the core package. ↩︎
-
Surprise! I don’t! ↩︎
-
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 scopeswrite:media
(to upload photos) andwrite:statuses
(to make posts). ↩︎