Let’s set up a Mastodon application with Python to read and post toots.
How?
Python is the second best tool for any job in 2021, which makes it an excellent glue language. I’ve been centering my site workflow around it. That means the Mastodon.py library, which I have dabbled with once or twice before.
Why?
Because I’ve let the IndieWeb social aspects of this site go stale and one step to fixing that is restoring POSSE automation. The first part of that is making sure I remember how to automate posting to Mastodon.
Ok fine; get on with it
Course, you’re going to need an account at a Mastodon instance. I have my account on one of many. You can find one suitable for your tastes at Mastodon instances.
ℹ️
Note
If you don’t already know Mastodon, think of it as island versions of Twitter. Each instance has its own practices and policies depending on who runs it, so it’s very much a “hanging out at a friend’s house” experience. Lots more details, but much more than I feel like covering.
It’s fun. You should try it out maybe. You can even host your own instance if you’re hard-core into DIY.
Registering your application
I have 2FA enabled, so it turned out to be easier for me to set up the application in account preferences (under the “Development” section).
I entered an application name, added my Website for “Application website,” and selected the scopes that are important to me for today’s explorations.
read
read all your account’s data
write:statuses
publish statuses
That’s enough to cover today’s play. I’m not creating my own full-fledged Mastodon client so I don’t need every permission.
Spoiler alert: yes I’ll be using Rich and dataclasses along with Mastodon.py. Nothing fancy planned with Rich today. It’s just part of my regular toolkit.
The dataclasses library comes standard with Python these days, but you may need to install the others:
pip install --upgrade rich mastodon
The Mastodon instance developer panel gives me the details I need to connect. I set them as workspace environment variables with direnv out of habit, but you could just as easily hard-code them in Python or define in a config file of your own.
From my first few attempts writing this post, I know I’ll want a class to organize views for the connection.
@dataclassclassApp:"""Provides convenience methods for querying an instance and posting toots."""mastodon:Mastodon
Once I have a connection, I don’t care about those application config details. Rather than storing them in the instance, I’ll use a class method to handle the work and return my new App with only the details I do care about.
classApp:@classmethoddefconnect(cls,client_key:str=CLIENT_KEY,api_base_url:str=API_BASE,client_secret:str=CLIENT_SECRET,access_token:str=ACCESS_TOKEN,)->"App":"""Return an App connected to a specific Mastodon instance."""mastodon=Mastodon(client_id=client_key,api_base_url=api_base_url,client_secret=client_secret,access_token=access_token,)returncls(mastodon=mastodon)
Basic setup’s done. Let’s create an App and see if it worked.
app=App.connect()ifapp.mastodon.instance_health():rich.print("Connection instance is [green]healthy[/green]")else:rich.print("Connection instance is [red][b]not[/b] healthy![/red]")sys.exit(1)
I don’t feel like showing every item in that dictionary, though. Let’s pick a few to make a decent summary. Oh hey, and let’s cache that dictionary to disk so I’m not making a fresh API query every time I check this post while I’m writing it.
💡
Tip
Be considerate about server resources for Mastodon. Most instances are run as personal projects. There’s no need for us to run up their AWS bill.
defstored(func:Callable)->Dict[str,Any]:definner(*args,**kwargs):filename=f"{func.__name__}.json"rich.print(f"stored.inner for {func.__name__}")ifos.path.exists(filename):withopen(filename,"r")asf:rich.print(f"Loading data from {filename}")data=json.load(f)returndatarich.print(f"Calling {func.__name__}")data=func(*args,**kwargs)withopen(filename,"w")asf:rich.print(f"Writing data to {filename}")json.dump(data,f,indent=4,default=str)returndatareturninner
I can do proper memoization later. “Look for a file before you hit the server” is good enough for writing a blog post.
classApp:@storeddefinstance(self)->Dict[str,Any]:"""Return a dictionary of information about the connected instance."""returnself.mastodon.instance()definstance_summary(self)->Dict[str,Any]:"""Return a small dictionary of instance information."""instance=self.instance()fields=["uri","title","short_description"]data={field:instance[field]forfieldinfields}data["contact_account"]=instance["contact_account"]["display_name"]returndata
Time to look at that instance summary.
if__name__=="__main__":app=App.connect()ifapp.mastodon.instance_health():rich.print("Connection instance is [green]healthy[/green]")else:rich.print("Connection instance is [red][b]not[/b] healthy![/red]")sys.exit(1)pprint(app.instance_summary())
Connection instance is healthy
stored.inner for instance
Calling instance
Writing data to instance.json
{│ 'uri': 'hackers.town',
│ 'title': 'hackers.town',
│ 'short_description': "A bunch of technomancers in the fediverse. Keep it fairly clean please. This arcology is for all who wash up upon it's digital shore.",
│ 'contact_account': 'The_Gibson'}
Reading the timelines
Mastodon’s timeline methods provide different views of recent post activity, both public and private. To simplify demonstration on this public blog post, I’ll stick to timeline_public.
if__name__=="__main__":app=App.connect()ifapp.mastodon.instance_health():rich.print("Connection instance is [green]healthy[/green]")else:rich.print("Connection instance is [red][b]not[/b] healthy![/red]")sys.exit(1)pprint(app.instance_summary())pprint(app.timeline_summary(),max_string=80)
Connection instance is healthy
stored.inner for instance
Loading data from instance.json
{│ 'uri': 'hackers.town',
│ 'title': 'hackers.town',
│ 'short_description': "A bunch of technomancers in the fediverse. Keep it fairly clean please. This arcology is for all who wash up upon it's digital shore.",
│ 'contact_account': 'The_Gibson'}
stored.inner for timeline_public
Calling timeline_public
Writing data to timeline_public.json
[ .. skipping a few ...│ {│ │ 'date': '2021-08-15 22:24:35+00:00',
│ │ 'author': 'Endless Screaming',
│ │ 'content': '<p>AAAAAAAAAAAAAAAAAAAAH</p>'│ },
│ {│ │ 'date': '2021-08-15 22:24:43.531000+00:00',
│ │ 'author': 'Lynne',
│ │ 'content': '<p>This just touched a single topic that I’ve never heard being brought up anywh'+97│ }]
Nice. Looks like content is in HTML format. Need to remember that if I ever make a more interesting Mastodon client.
But I’m ready to start tooting.
Writing
[write-methods]:
Mastodon write methods let us add toots, polls, replies, reblogs, faves. All that good stuff.
Let’s stick with your basic toot for now.
classApp:defstatus_post(self,status:str,visibility:str="direct")->Dict[str,Any]:"""Post a toot to our connection, private unless we say otherwise."""returnself.mastodon.status_post(status,visibility=visibility)
if__name__=="__main__":app=App.connect()ifapp.mastodon.instance_health():rich.print("Connection instance is [green]healthy[/green]")else:rich.print("Connection instance is [red][b]not[/b] healthy![/red]")sys.exit(1)status_text="Ignore me, just messing with Mastodon.py"app.status_post(status_text)
It worked!
Okay my brain is fading. Should probably put away the keyboard soon.
Wrap it up
Am I done?
Well, no. I still need to turn this into a proper command line application that looks for the newest published blog post and posts a toot about it. But that’s not going to happen in today’s post.