Autotooting with Mastodon
August 25, 2018 at 11:06 PM by Dr. Drang
Earlier this week I followed the cool kids to Mastodon and started an account, which means I have to start playing around with the API. In particular, I wanted to have a script that would toot1 a link to a recently published blog post. Here are the steps I followed to write one.
First, I created an app from the command line using cURL:
curl -X POST -d "client_name=leantusk&redirect_uris=urn:ietf:wg:oauth:2.0:oob&scopes=write read&website=http://leancrew.com" https://mastodon.cloud/api/v1/apps
Let’s break this down. To register my Mastodon app, I post the necessary data to the apps
method of the API hosted on mastodon.cloud
, which is the instance (i.e., server) my account is at. That explains the URL at the end of the line and the -X POST
switch.
The string argument of the -d
switch is the data that the apps
method needs. The individual pieces of data are (and have to be) separated by ampersands in the curl
invocation, but they’re easier to read and understand if we split them into separate lines:
client_name=leantusk
redirect_uris=urn:ietf:wg:oauth:2.0:oob
scopes=write read
website=http://leancrew.com
- The client name is arbitrary, but some Mastodon client apps will show it, so don’t choose something that will embarrass you.
- The weird redirect URI is what the API docs say you should use if you don’t want the user to be redirected after authorization. For single purpose apps like mine, this is the correct choice.
- The script I’m going to build is for writing toots, but I might use the same app registration for other things, so I added
read
, too. - The app’s website is here.
After running this command, I got a JSON reply that looked like this:
{"id":"207492",
"name":"leantusk",
"website":"http://leancrew.com",
"redirect_uri":"urn:ietf:wg:oauth:2.0:oob",
"client_id":"alongstringofcharacters",
"client_secret":"anotherlongstring"}
What we’ll need for the next step are the client_id
and client_secret
strings, which look like ASCII hex dumps to me. They’re both 64 characters long, which presumably translates to 32 bytes.
The second step of registration is to get an authorization token, which will be included in every request made by the script to the API. The registration and authorization of an app is broken into two steps because in the usual case the app maker and the user are two different people.
I got my token through another invocation of cURL:
curl -X POST -d "client_id= alongstringofcharacters&client_secret= anotherlongstring&scope=write read&grant_type=password&username=myusername&password=mypassword" https://mastodon.cloud/oauth/token
Here, the information is posted to oauth/token
at my instance, and the data can be broken down into these parts:
client_id= alongstringofcharacters
client_secret= anotherlongstring
scope=write read
grant_type=password
username=myusername
password=mypassword
- The
client_id
andclient_secret
came from the previous step. - The
scope
is the same as before. Why do we need to repeat this? I assume it’s because app users may not want to authorize all the scopes an app can deliver. More mysterious to me is why this time it’s called “scope” when the last time it was called “scopes.” - The
grant_type
means we’re using a password to authorize the app. - The
username
andpassword
are mine.
The output of this command looked like
{"access_token":"another32bytehexdump",
"token_type":"Bearer",
"scope":"write read",
"created_at":1234567890}
where the key piece of information is the access_token
. This will be included in the header to every request my script makes to the API.
Here’s the script, which I call tootpost
:
python:
1: #!/usr/bin/env python
2:
3: import requests
4: from os import environ
5:
6: # Mastodon information
7: token = "another32bytehexdump"
8: postURL = 'https://mastodon.cloud/api/v1/statuses'
9:
10: # The snowman prefix.
11: # Currently, Mastodon's website messes up the display of the
12: # snowman emoji, ⛄️, because the variation selector, U+FE0F,
13: # appears as a box. This code constructs a string for the snowman
14: # without the variation selector. It's possible this won't display
15: # properly on some clients or platforms.
16: prefix = b'\xe2\x9b\x84'.decode()
17:
18: # Assemble the toot content.
19: home = environ['HOME']
20: lastPostInfo = open(home + '/path/to/all-posts.txt').readlines()[-1]
21: (date, time, slug, title) = lastPostInfo.split(' ', 3)
22: title = title.rstrip()
23: path = date[:-3].replace('-', '/')
24: leanURL = 'http://leancrew.com/all-this/{}/{}/'.format(path, slug)
25: toot = '''{} {}
26: {}'''.format(prefix, title, leanURL)
27:
28: # Send the toot.
29: header = {'Authorization': 'Bearer {}'.format(token)}
30: payload = {'status': toot}
31: r = requests.post(postURL, data=payload, headers=header)
32:
33: print(r.json()['uri'])
Every time I publish a new post here, my publishing system adds a new line to a file called all-posts.txt
. Each line of the file looks like this:
2018-08-19 09:42:11 message-image-to-family Message Image to Family
The date and time of publication are the first two items. Then comes the “slug,” which is a URL-friendly transformation of the title. After the slug comes the post title itself.
Lines 18–26 get the last line of all-posts.txt
and construct the content of the toot from it. For the post shown above, that content would be
⛄️ Message Image to Family
http://leancrew.com/all-this/2018/08/message-image-to-family
Putting the snowman emoji into the toot turned out to be trickier than it should have been. When I first wrote the script, Lines 25–26 looked like this:
python:
25: toot = '''{} {}
26: {}'''.format('⛄️', title, leanURL)
This, I soon learned, led to display problems when viewing the toot on the website in a browser. The snowman emoji came with a trailing box.
This didn’t seem to happen in Mastodon client apps that friends and I tried, and it was clearly a bug, but it was a bug that I had to work around. Lots of people access Mastodon through their browser, and I couldn’t have my toots looking like that.
Jason Biatek put me on the trail to a solution. When I use the emoji picker to add the snowman to my code, it puts a special Unicode character, Variation Selector-16, after the snowman. This is supposed be a nonprinting character that says “display the preceding character as an emoji instead of as a monochrome glyph.” It’s certainly displaying the emoji, but it’s screwing up the “nonprinting” part.
Line 16 is my workaround. After some playing around with various encode
and decode
commands in Python’s interactive mode, I came up with the three-byte string for the snowman emoji without the variation selector. And I wrote a long comment so future me would understand why I’m using bytes to handle what should be a simple string.
The toot gets sent on Lines 29–31 using
Kenneth Reitz’s wonderful requests
library. Line 29 constructs the header with the authorization token from Line 7. Line 30 constructs the data to be POSTed. And Line 31 does the posting to my instance’s API, using the URL from Line 8.
Finally, Line 33 takes the JSON output returned from the API call, extracts the uri
, and prints it. This is the URL of the toot just sent.
Mastodon’s API docs are pretty good, but I couldn’t find any single place that had both steps of the app creation/authorization procedure. Now I can.
-
I am not thrilled with “toot” as Mastodon’s answer to “tweet.” Likewise, I don’t think “instance” is a more friendly term than “server.” Many of Mastodon’s decisions seem odd, especially if you’ve spent a long time on Twitter, but it’s not nearly as confusing as some would have you believe. The best introduction to the practical aspects of going from Twitter to Mastodon is Joe Steel’s. ↩