How far did my post go on the Fediverse?
I wrote a moderately popular post on Mastodon. Lots of people shared it. Is it possible to find out how many different ActivityPub servers it went to?
Yes!
As we all know, the Fediverse is one big chain mail. I don't mean that in a derogatory way.
When I write a post, it appears on my server (called an "instance" in Mastodon-speak).
Everyone on my instance can see my post.
My instance looks at all my followers - some of whom are on completely different instances - and sends my post to their instances.
As an example:
- I am on
mastodon.social
- John is on
eggman_social.com
- Paul is on
penny_lane.co.uk
- Both John and Paul follow me. So my post gets syndicated to their servers.
With me so far?
What happens when someone shares (reposts) my status?
- John is on
eggman_social.com
- Ringo is on
liverpool.drums
- Ringo follows John
- John reposts my status.
eggman_social.com
syndicates my post toliverpool.drums
And so my post goes around the Fediverse! But can I see where it has gone? Well... sort of! Let's look at how.
A note on privacy
People on Mastodon and the Fediverse tend to be privacy conscious. So there are limits - both in the API and the culture - as to what is acceptable.
Some people don't share their "social graph". That is, it is impossible to see who follows them or who they follow.
Users can choose to opt-in or -out of publicly sharing their social graph. They remain in control of their privacy.
In the example above, if Ringo were to reshare John's reshare of my status - John doesn't know about it. Only the original poster (me) gets notified. If John doesn't share his social graph, it might be possible to work out where Ringo saw the status - but that's rather unlikely.
Mastodon has an API rate limit which only allows 80 results per request and 1 request per second. That makes it long and tedious to crawl thousands of results.
Similarly, some instances do not share their social data or expose anything of significance. Some servers may no longer exist, or might have changed names. It's impossible to get a comprehensive view of the entire Fediverse network.
And that's OK! People should be able to set limits on what others can do with their data. The code you're about to see doesn't attempt to breach anyone's privacy. All it does is show me which servers picked up my post. This is information which is already shown to me - but this makes it slightly easier to see.
The Result
I looked at this post of mine which was reposted over 100 times.
It eventually found its way to… 2,547 instances!
Ranging from 0ab.uk
to թութ.հայ
via godforsaken.website
and many more!
And that's one of the things which makes me hopeful this rebellion will succeed. There are a thousand points of light out there - each a shining beacon to doing things differently. And, the more the social media giants tighten their grip, the more these systems will slip through their fingers.
The Code
This is not very efficient code - nor well written. It was designed to scratch an itch. It uses Mastodon.py to interact with the API.
It gets the instance names of all my followers. Then the instance names of everyone who reposted one of my posts.
But it cannot get the instance names of everyone who follows the users who reposted me - because: The only way to get a list of followers from a user on a different instance is to apply for an API key for that instance. Which seems a bit impractical.
But I can get the instance name of the followers of accounts on my instance who reposted me. Clear?
I can also get a list of everyone who favourited my post. If they aren't on my instance, or one of my reposter's follower's instances, they're probably from a reposter who isn't on my instance.
My head hurts.
Got it? Here we go!
Python 3import config
from mastodon import Mastodon
from rich.pretty import pprint
# Set up access
mastodon = Mastodon( api_base_url=config.instance, access_token=config.access_token, ratelimit_method='pace' )
# Status to check for
status_id = 111040801202691232
print("Looking up status: " + str(status_id))
# Get my data
me = mastodon.me()
my_id = me["id"]
print("You have User ID: " + str(my_id))
# Empty sets
instances_all = set()
instances_followers = set()
instances_reposters = set()
instances_reposters_followers = set()
instances_favourites = set()
# My Followers
followers = mastodon.account_followers( my_id )
print( "Getting all followers" )
followers_all = mastodon.fetch_remaining( followers )
print("Total followers = " + str( len(followers_all) ) )
# Get the server names of all my followers
for follower in followers_all:
if ( "@" in follower["acct"]) :
f = follower["acct"].split("@")[1]
instances_all.add( f )
if ( f not in instances_followers):
print( "Follower: " + f )
instances_followers.add( f )
else :
instances_all.add( "mastodon.social" )
instances_followers.add( "mastodon.social" )
total_followers = len(instances_followers)
print( "Total Unique Followers Instances = " + str(total_followers) )
# Reposters
# Find the accounts which reposted my status
reposters = mastodon.status_reblogged_by( status_id )
reposters_all = mastodon.fetch_remaining(reposters)
# Get all the instance names of my reposters
for reposter in reposters_all:
if ( "@" in reposter["acct"]) :
r = reposter["acct"].split("@")[1]
instances_all.add( r )
if ( r not in instances_followers ) :
print( "Reposter: " + r )
instances_reposters.add( r )
total_reposters = len(instances_reposters)
print( "Total Unique Reposters Instances = " + str(total_reposters) )
# Followers of reposters
# This can take a *long* time!
for reposter in reposters_all:
if ( "@" not in reposter["acct"]) :
reposter_id = reposter["id"]
print( "Getting followers of reposter " + reposter["acct"] + " with ID " + str(reposter_id) )
reposter_followers = mastodon.account_followers( reposter_id )
reposter_followers_all = mastodon.fetch_remaining( reposter_followers )
for reposter_follower in reposter_followers_all:
if ( "@" in reposter_follower["acct"]) :
f = reposter_follower["acct"].split("@")[1]
instances_all.add( f )
if (f not in instances_reposters_followers) :
print( " Adding " + f + " from " + reposter["acct"] )
instances_reposters_followers.add( f )
total_instances_reposters_followers = len(instances_reposters_followers)
print( "Total Unique Reposters' Followers Instances = " + str(total_instances_reposters_followers) )
# Favourites
# Find the accounts which favourited my status
favourites = mastodon.status_favourited_by( status_id )
favourites_all = mastodon.fetch_remaining(favourites)
# Get all the instance names of my favourites
for favourite in favourites_all:
if ( "@" in favourite["acct"]) :
f = favourite["acct"].split("@")[1]
instances_all.add( f )
if ( f not in instances_favourites ) :
print( "Favourite: " + f )
instances_favourites.add( r )
total_favourites = len(instances_favourites)
print( "Total Unique Favourites Instances = " + str(total_favourites) )
print( "Total Unique Reposters Instances = " + str(total_reposters) )
print( "Total Unique Followers Instances = " + str(total_followers) )
print( "Total Unique Reposters' Followers Instances = " + str( len(instances_reposters_followers) ) )
print( "Total Unique Instances = " + str( len(instances_all) ) )
Ben Thompson said on mastodon.scot:
@Edent have you seen the "view reach" function on @trunksapp ?
McNeely said on indieweb.social:
@Edent Love it! I thought about doing this for my one post that accidentally took off but never got around to it. Cool to see it actually being done
Ben Tasker said on mastodon.bentasker.co.uk:
@Edent It's not (nearly) as tidy as your approach, but you can also infer general reach information from your access logs - each receiving instance will have requested the page in order to build a preview card.
More comments on Mastodon.