Posting text and images to Mastodon

Tutorial – Mastodon

Author(s):

Creating a custom application that toots text to Mastodon (the Fediverse's version of Twitter) is simple and straightforward. But we can mix it up by adding images and video, scheduling posts, and changing privacy settings.

If you haven't read the prior article in this series [1], you may want to do that now. There, you will learn how to read from a Mastodon feed and print out the toots (the name Mastodon gives its posts) onto the command line. You can do that quite easily with Python, because the API for Mastodon is open and well documented, and also because there is a comprehensive Python wrapper [2] that makes building ad hoc clients a breeze.

In this article, we're going a step further, and you will see how to create an app that posts your toots. Before you start, and just in case you haven't already, install the Mastodon.py module with

pip install Mastodon.py

and let's get started.

Registering

As with your reading application, the first thing you have to do is register your application with Mastodon (Listing 1).

Listing 1

posttoots_register.py

01 #!/usr/bin/env python
02
03 from mastodon import Mastodon
04
05 Mastodon.create_app(
06      'posttoots',
07      api_base_url = 'https://your.mastodon.server',
08      scopes=['read', 'write'],
09      to_file = '.secrets'
10 )
11
12 mastodon = Mastodon (
13     client_id = '.secrets',
14 )
15
16 print ('Mastodon version: ' + mastodon.retrieve_mastodon_version())

You only have to run this once. It registers the posttoots application (line 6) with https://your.mastodon.server (line 7 – insert the server hosting your account here), sets its scope to 'read' and 'write' so you can both post to an account and also read information off the server (line 8), and grabs some credentials and stores them in a hidden file called .secrets (line 9).

Although it does not affect the registration, you can tack on lines 12 through 16. Lines 12, 13, and 14 check to see if the app can be activated using the downloaded credentials in .secrets and line 16 tells you what version of Mastodon your server is using. This can be useful if your program relies on features available on the very latest Mastodon, but your server has not updated yet.

You only need to run posttoots_register.py once as you can use the credentials stored in .secrets over and over. .secrets (Listing 2) contains a client_id (line 1) and a client_secret (line 2) that you will be able to use to identify your application each time it has to interact with the Mastodon instance. On line 3 you also have the address of the instance the app is registered with, which is convenient as you will see later on.

Listing 2

.secrets

01 53eM1n9lyHR4nd0mUnUMB3r5vAN6G133TeR5X4g
02 KM0r340FdTh3p54M3A60nTnUkKN0WC0hweMigLcng6U
03 https://your.mastodon.server

So, posting is very straightforward. Consider Listing 3.

Listing 3

posttoots.py (Basic)

01 #!/usr/bin/env python
02
03 import sys
04 from mastodon import Mastodon
05
06 mastodon = Mastodon (
07     client_id = '.secrets',
08 )
09
10 mastodon.access_token = mastodon.log_in (
11     username = sys.argv[1],
12     password = sys.argv[2],
13     scopes = ['read', 'write']
14 )
15
16 mastodon.status_post (sys.argv[3])

After pulling in sys (to process the command-line arguments – line 3) and Mastodon (for everything else – line 4), you activate your app with the Mastodon server (lines 6 through 8). You could insert here the client_id, client_secret, and the host, but we have all that in the .secrets file, remember? So all you have to do is point to that (line 7).

Then you need to log in with your username and password so the application posts to a specific account (lines 10 through 14). Pass your username and password to the application and the value of scopes. I am not sure why you have to add the latter, but if you don't, the login fails. The value of scopes must be the same as what you used when registering your application in Listing 1. Finally, on line 16, you post a message using Mastodon.py's status_post() function.

You can use posttoots.py like this:

./posttoots.py youremail@here.com "Your awesome password" "Tooting from the command line with my own app!"

And "Tooting from the command line with my own app!" will pop up in your feed (Figure 1).

Figure 1: Tooting plain text to your account requires a simple Python program.

That's it. The end. See you next issue…

Beyond Simple

Well, not really, because there is so much more you can do when posting. You can, for example, append an image or a video, schedule your toots so they are posted automatically at a later date (easier said than done), set the visibility of each toot, and quite a bit more. To see how all of the above is achieved, consider Listing 4.

Listing 4

posttoots.py (Advanced)

01 #!/usr/bin/env python
02
03 import datetime
04 import cv2
05 from optparse import OptionParser
06 from mastodon import Mastodon
07
08 class post:
09     def __init__ (self, vUser, vPassword):
10         f_read = open(".secrets", "r")
11         instance = f_read.readlines()[-1].strip(" \n")
12         f_read.close()
13
14         self.mastodon = Mastodon(
15             client_id = '.secrets',
16             api_base_url = instance
17         )
18
19         self.mastodon.access_token = self.mastodon.log_in(
20             username = vUser,
21             password = vPassword,
22             scopes = ['read', 'write'],
23             to_file = '.token'
24         )
25
26         self.post_date = None
27
28     def send (self, vToot, vMedia, vVis):
29         self.mastodon.status_post(vToot, media_ids = vMedia, scheduled_at = self.post_date, visibility = vVis)
30
31     def add_Media (self, vMedia):
32         return (self.mastodon.media_post (vMedia))
33
34     def set_date (self, vDate):
35         try:
36             self.post_date = datetime.datetime.strptime(vDate, '%b %d %Y %H:%M %Z')
37         except:
38             self.post_date = None
39
40     def show_scheduled (self):
41         print (self.mastodon.scheduled_statuses())
42
43 if __name__ == '__main__':
44     parser = OptionParser()
45     parser.add_option ('-a', '--action',    help = '"post", "schedule" or "list"',  dest = 'vAction',   default = 'list')
46     parser.add_option ('-u', '--username',  help = 'user\'s email',     dest = 'vUser')
47     parser.add_option ('-p', '--password',  help = 'user\'s password',  dest = 'vPassword')
48     parser.add_option ('-t', '--toot',      help = 'toot text',         dest = 'vToot')
49     parser.add_option ('-m', '--media',     help = 'media ("cam" or image, video, etc.)', dest = 'vMedia', default = None)
50     parser.add_option ('-w', '--when',      help = 'when to publish toot. Format: "mmm dd YYYY HH:MM Z"', dest = 'vWhen', default = None)
51     parser.add_option ('-v', '--visibility',help = '"public", "unlisted", "private" or "direct"', dest = 'vVis', default = 'public')
52
53     (options, args) = parser.parse_args()
54
55     p=post(options.vUser, options.vPassword)
56
57     mmedia = options.vMedia
58     if mmedia != None:
59         if mmedia == "cam":
60             camera=cv2.VideoCapture(0)
61             return_value, image =camera.read()
62             cv2.imwrite("pic.png", image)
63             del(camera)
64             mmedia="pic.png"
65
66         mmedia = p.add_Media(mmedia)
67
68     if options.vAction == 'post':
69         p.send (options.vToot, mmedia, options.vVis)
70
71     elif options.vAction == 'schedule':
72         p.set_date (options.vWhen)
73         p.send (options.vToot, mmedia, options.vVis)
74
75     else:
76         p.show_scheduled()

You can divide the program into 5 distinct sections:

  1. Line 1 sets the interpreter for the program.
  2. Lines 3 through 6 pull in the modules you'll need. The datetime module lets you convert strings to dates. You will need this when you want to parse dates passed on by the user to schedule toots. The cv2 module is the Python implementation of OpenCV [3], the library that will allow you to capture an image from your webcam. As you need to parse quite a few parameters to make life easier for the user, let's use optparse too. Finally, we import the mastodon module.
  3. Lines 8 through 41 is the class that does all the dirty work of activating the app, posting, and scheduling.
  4. Lines 44 through 53 collect and allocate all the options the user inputs from the command line.
  5. Finally, lines 55 through 76 call the functions we need depending on the input from the user.

To go into detail, let's see how we can post an image within a toot.

Posting Pictures

One nice feature that comes bundled with optparse is how it provides help for the user. Run this:

./ postoots.py -h

And you'll get a summary of the possible options and hints as to how to use them.

To start with, -h tells you to input your username (the email associated with your Mastodon account) with the -u option and your password with the -p option.

The -a option tells your app what you want to do. You can choose between post (send a toot), schedule (schedule a toot for later), or list (list all the scheduled toots). As you want to send a toot with an image, use post here.

The -t option contains the text for the toot, and -m contains the path and name of the media (picture) you want to send. Given all of the above, your complete command line will look like this:

./posttoots.py -u youremail@here.com -p "Your awesome password" -a post -t "Tooting an image from the command line with my own app!" -m /path/to/image.png

Looking at Listing 4, once you hit Enter, the pairs of options and values from the command-line arguments get dumped into (options, args) (line 53), and the Mastodon object is initiated with your username and password (line 55). The initialization function (lines 9 to 26), takes the URL of the Mastodon instance, client_id and client_secret from the .secrets file (lines 10 to 17), logs the application in (lines 19 to 24), and sets the post_date attribute to None (line 26).

You will have noticed that the program gets the URL of the Mastodon instance in a very roundabout way. Instead of just loading all the values in from .secrets, including the URL, into client_id, we do this convoluted thing of opening the .secrets file (line 10), dumping the contents of the last line into instance (line 11), and then closing the file (line 12).

The reason for this is that there seems to be a bug in Mastodon.py that appears when you try to use scheduling: If you rely on a file to load in your instance's URL, anything to do with scheduling will fail with an error complaining about the version of Mastodon running on the server, or some such nonsense. The workaround is setting the URL explicitly on line 16.

This is not the only problem with scheduling as we will see later, but let's get back down into the main body of the program where, on line 58, you check to see if the user has given a path and filename of a media file. If they have, you add the media file to the toot object (line 66 and then lines 31 and 32).

Finally the toot, along with the media, is sent on line 69 by calling the actual Mastodon sending function on lines 28 and 29 (Figure 2).

Figure 2: You can also send images and videos to your feed relatively easily.

You can add up to four images to a toot, as well as videos and other kinds of files.

Private Post

Notice that, if using cam with the -m argument (line 59), you have a block of code (lines 60 to 64) that uses methods from the cv2 module to grab a picture from the default webcam and send that to Mastodon.

Remembering that you pulled in the OpenCV module (cv2) on line 4, you then create a VideoCapture object from the default camera on line 60, read what the camera is seeing on line 61, write it out to a file on line 62, and free up the camera again when done (line 63). All that's left to do is to dump the location of the image into the mmedia variable for posting (line 64).

Posting an image from a webcam may be useful for, say, monitoring your home, but it does raise serious privacy concerns if the posts are public. This is where a toot's visibility options come into play. On line 51 of Listing 4, you can see that a user can set a toot's visibility. The options available are:

  • Public means the toot will be visible to everyone. A public toot can also be found when running a search across the Mastodon federated network.
  • Unlisted means the toot is visible, but that it cannot be found via a search. Your followers can see it, but a non-follower would need a direct link to the toot to be able to read it. An unlisted toot can be boosted ("retooted"), but the boosted toot remains unlisted.
  • Private is a bit confusing, because it may not mean what you think. Your followers can see private toots, but non-followers can't. So, in fact, it's not very private at all. However, you cannot boost another user's private toot.

Figure 3 shows three toots with different levels of privacy. Notice the padlock and the absence of a boosting icon in the private toot.

Figure 3: Three toots with different levels of privacy.

There is a fourth level of privacy which, it turns out, is exactly what you need to keep the pictures from your webcam truly private:

  • Direct makes the toot a direct message and these are truly private, since they land in your inbox and only you and the owners of the accounts mentioned in the message can see them.

So if you send a direct message with a snap attached but no username (Figure 4, bottom toot) or your own username in the text of the message (Figure 4, top toot), they will land discretely in your Mastodon inbox and won't be seen by anyone else:

./posttoots.py -u youremail@here.com -p "Your awesome password" -a post -t "Direct test toot with pic" -m cam -v direct
Figure 4: Sending a direct message is your best bet for total privacy.

That said, you will have to click on a link to see the image. But, if you have two separate Mastodon accounts and send a direct message from one to the other, the toot will show up in the recipient's feed with a preview of the image, like you can see in Figure 5.

./posttoots.py -u youremail@here.com -p "Your awesome password" -a post -t "Direct test toot to @your_other@account"  -m cam -v direct
Figure 5: Direct messages to your account show up in your feed, but only you can see them.

The darker background indicates that it is a direct message and not a regular toot.

Post-posting

Warning: Scheduling is finicky and seems to be supported on only a few Mastodon instances. But, if one day scheduling on Mastodon becomes universally available, this is how you would do it:

Set the -a option to schedule and set the -w argument. The -w argument takes a string that contains a date. The format for the date is "mmm dd YYYY HH:MM Z", where mmm is a three letter abbreviation of the month in lower case (jan, feb, mar, etc.), dd is the day of the month (01 to 31), YYYY is the year (2019, 2020, etc.), HH is the hour (00 to 23), MM are the minutes (00 to 60), and Z is the time zone (UTC, CET, PDT, etc.).

To send a toot at noon on the 31st of January 2020, for example, you'd run:

./posttoots.py -w "jan 31 2020 12:00 CET"  -a schedule  -u youremail@here.com  -p "Your awesome password"  -t "Scheduled from command line at 12:00"

In Listing 4, lines 71 to 73 are the ones that deal with the schedule option. First, the scheduled date is processed on lines 34 to 38 using Python's datetime module if it is a viable date (lines 35 and 36). However, the string will be discarded if datetime cannot identify it as a date (lines 37 and 38). Then the toot is sent as usual (Figure 6).

Figure 6: Scheduled toots show up in your timeline at the preprogrammed time. You can also send them as direct messages.

You can see the list of scheduled toots by using the list option with the -a argument:

./posttoots.py -a list -u youremail@here.com -p "Your awesome password"

Scheduling is cool, but as mentioned above, fickle. As noted elsewhere, if you rely on your .secrets file to supply the address of the Mastodon instance you are using, for some reason, nothing related to scheduling will work, and your app will bomb out complaining about the version of the instance. That's the reason for the ugly hack on lines 10 through 12 in Listing 4.

And then there's the fact that not all Mastodon instances support scheduling, so don't be surprised if the above does not work for you. If you try to schedule a post from your command line and then it doesn't show up in your feed at the designated time, it is a pretty sure indication that the server does not support this feature. If scheduling toots is important for you, it will be time to search for another server.

You can try https://mastodon.social/. This is the server maintained by Eugen Rochko, one of the creators of Mastodon and the person who implemented scheduling. Scheduling works a treat on this instance.

Conclusion

This is the end of the series dedicated to Mastodon. As you may have gathered, Mastodon is pretty great, and it is easy to see why it has become the most popular service in the Fediverse. It is more flexible than Twitter and much easier to program for. At this point, you should have enough knowledge to do all sorts of things, from creating clients to building bots and becoming a nuisance.

But that does not mean we are done. We still have to explore all the other services, the increasingly popular PeerTube, a contender for YouTube; Pixelfed, which wants to become the Instagram of the federated social media; and so many more. We'll be tackling these and exploring how to best leverage them in upcoming issues.

See you in the Fediverse!

Infos

  1. "Build your own Mastodon client," by Paul Brown, Linux Magazine, issue 230, January 2020, pp. 92-94
  2. Mastodon.py documentation: https://mastodonpy.readthedocs.io/en/stable/index.html
  3. OpenCV: https://opencv.org/