A rootin’-tootin’ RSS aggregator.
Turning an RSS feed into a Mastodon toot.
I follow quite a few RSS feeds.
Games, news, that sort of thing.
So how to aggregate them all into a Mastodon feed?
To create a Mastodon account that was chock full of, say, games news?
Send a single toot from an RSS feed
The workflow concept is quite straightforward.
The python code takes an RSS feed.
It takes the latest post in the feed.
If that post is different to the last post tooted, it crafts a Mastodon toot announcing the new feed content, and updates the Mastodon timeline.
A cron job then runs every 15 minutes to loop through this workflow, picking up and tooting any new additions to the RSS feed.
The Mastodon API bit to authorise sending a toot
I’ve already covered how authorise access to the Mastodon API in an earlier post .
The same process can be followed again to allow the Python code below to write to Mastodon.
I have one API access token per feed, and while not strictly necessary, and is slightly more of a time investment it does compartmentalise each accounts access.
secrets.py
MASTODON_URL = 'https://foo-bar.com'
MASTODON_TOKEN = 'token string'
MASTODON_STATUS = MASTODON_URL + '/api/v1/statuses'
MASTODON_MEDIA = MASTODON_URL + '/api/v1/media'
FEED_URL = 'https://www.foo-bar.com/rss/'
FILE_PATH = '/home/foo-bar/'
FILE_NAME = 'logo.jpg'
FILE_DESC = 'Online Logo Description'
bot_with_image.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# A bot that takes an RSS feed and posts to Mastodon
# Parses the latest entry in the RSS feed and if it doesn't match the last toot, sends the latest entry
from secrets import *
from pathlib import Path
import requests
import feedparser
RSS_FEED = feedparser.parse(FEED_URL)
RSS_ENTRY = RSS_FEED.entries[0]
# Function to build the toot, send it and update the 'last_toot.txt' and 'last_post.txt' file
def send_toot():
# Send image file to 'MASTODON_MEDIA' to generate a 'media_id'
media_ids = []
media = Path(FILE_PATH) / FILE_NAME
# Build the authorisation header
auth = {
"Authorization" : f"Bearer {MASTODON_TOKEN}"
}
# Define the kind of file to be sent
media = {
"file": (FILE_NAME, media.open('rb'), 'application/octet-stream')
}
# Add a description to the file (probably a logo / image)
desc = {
"description": FILE_DESC
}
# Send toot (to 'MASTODON_MEDIA') and collect the json response
r = requests.post(MASTODON_MEDIA, headers=auth, files=media, data=desc)
json_data = r.json()
# This is the id identifier of the file just uploaded
media_id = json_data['id']
# Build the text part of the toot from the chosen 'RSS_ENTRY'
toot_str = ''
toot_str += f"{RSS_ENTRY['title']}\n\n"
toot_str += f"{RSS_ENTRY['description']}\n"
toot_str += f"\n\n{RSS_ENTRY['link']}\n\n"
toot_str += f"\n\nThis is an automated post from the #foo-bar RSS feed dated {RSS_ENTRY['published']}\n\n"
toot_str += f"\n\n#foo #bar #foo-bar\n\n"
# Build the payload, including the newly generated 'media_id'
payload = {
"status": toot_str, "media_ids[]": media_id
}
# Send toot (to 'MASTODON_STATUS') including the newly generated 'media_id'
requests.post(MASTODON_STATUS, headers=auth, data=payload)
# Update the 'last_toot.txt' file with 'latest_entry'
last_toot = open(Path(FILE_PATH) / "last_toot.txt","w")
last_toot.write(latest_entry)
last_toot.close()
# Update the 'last_post.txt' file with 'published' date
last_post = open(Path(FILE_PATH) / "last_post.txt","w")
last_post.write(published)
last_post.close()
# Output a response
print("New toot sent from the foo-bar RSS feed!")
# Find the latest RSS entry
latest_entry = RSS_ENTRY.id
# Find the latest published date
published = RSS_ENTRY.published
# Find the 'last_toot' sent
with open(Path(FILE_PATH) / "last_toot.txt") as last_toot:
last_toot = last_toot.read()
# If 'latest_entry' does not equal 'last_toot' then send a new toot based on 'latest_entry'
if latest_entry != last_toot:
send_toot()
elif latest_entry == last_toot:
# Output a response
print("Nothing new to add from the foo-bar RSS feed!")
Sending a stream of toots from an RSS feed
What if we want to populate a Mastodon timeline with a whole stream of RSS content?
The following works in a similar way to the ‘send a single toot’ approach, except it starts at the bottom of the RSS feed.
And then loops upwards through the feed items.
That way, the latest RSS post will never be the latest toot, until the Python loop has reached the start of the RSS content.
bot_all_with_image.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# A bot that takes an RSS feed and posts to Mastodon
# Parses the latest entry in the RSS feed and if it doesn't match the last toot, sends the latest entry
from secrets import *
from pathlib import Path
import requests
import feedparser
import time
RSS_FEED = feedparser.parse(FEED_URL)
# Function to build the toot, send it and update the 'last_toot.txt' and 'last_post.txt' file
def send_toot():
# Send image file to 'MASTODON_MEDIA' to generate a 'media_id'
media_ids = []
media = Path(FILE_PATH) / FILE_NAME
# Build the authorisation header
auth = {
"Authorization" : f"Bearer {MASTODON_TOKEN}"
}
# Define the kind of file to be sent
media = {
"file": (FILE_NAME, media.open('rb'), 'application/octet-stream')
}
# Add a description to the file (probably a logo / image)
desc = {
"description": FILE_DESC
}
# Send toot (to 'MASTODON_MEDIA') and collect the json response
r = requests.post(MASTODON_MEDIA, headers=auth, files=media, data=desc)
json_data = r.json()
# This is the id identifier of the file just uploaded
media_id = json_data['id']
# Build the text part of the toot from the chosen 'RSS_ENTRY'
toot_str = ''
toot_str += f"{RSS_ENTRY['title']}\n\n"
toot_str += f"{RSS_ENTRY['description']}\n"
toot_str += f"\n\n{RSS_ENTRY['link']}\n\n"
toot_str += f"\n\nThis is an automated post from the #foo-bar RSS feed dated {RSS_ENTRY['published']}\n\n"
toot_str += f"\n\n#foo #bar #foo-bar\n\n"
# Build the payload, including the newly generated 'media_id'
payload = {
"status": toot_str, "media_ids[]": media_id
}
# Send toot (to 'MASTODON_STATUS') including the newly generated 'media_id'
requests.post(MASTODON_STATUS, headers=auth, data=payload)
# Update the 'last_toot.txt' file with 'latest_entry'
last_toot = open(Path(FILE_PATH) / "last_toot.txt","w")
last_toot.write(latest_entry)
last_toot.close()
# Update the 'last_post.txt' file with 'published' date
last_post = open(Path(FILE_PATH) / "last_post.txt","w")
last_post.write(published)
last_post.close()
# How many RSS entries are there (-1 as the indexing starts at 0)
count = len(feedparser.parse(FEED_URL)['entries'])-1
# Loop through all of the RSS entries (until it is less than 0, where the indexing started)
print("There are ", count+1, " RSS entries to toot")
while count != -1:
latest_entry = RSS_FEED.entries[count].id
# Find the published date
published = RSS_FEED.entries[count].published
RSS_ENTRY = RSS_FEED.entries[count]
print("Tooting - " + RSS_FEED.entries[count].title + " ", count, " toots to go")
send_toot()
count = count -1
time.sleep(180)
# Output a response
print("All done!")
The following two files need to be in place and writable.
The files do not have to contain the exact content as below, they just need to exist so that the Python script can write:
- The last post from the RSS feed
- The last toot sent to Mastodon
last_post.txt
Wed, 12 Apr 2023 20:30:00 +0000
last_toot.txt
https://www.foo-bar.com/rss/100
cron job
I have more than one Python script set up and they are all called from a bash script every 15 minutes using cron.
This is what calling one of those jobs looks like.
# Run the Mastodon bots that turn RSS feeds into toots every N minutes
*/5 * * * * python /home/foo/Dev/RasPi/GamesFeedRSS/bot_with_image.py &
Finally, RSS content in a toot!
Screenshot of the GamesFeedRSS Mastodon Account
A real world example of the bot in action can be found at https://mstdn.social/@GamesFeedRSS to which new feeds are occasionally added.
All of the Mastodon code examples can also be found on Forgejo .