Cookie Consent by TermsFeed

Tracking Your Mastodon Follower/Following Changes with Python

Tracking Your Mastodon Follower/Following Changes with Python

At times, you might have observed the number of followers and following of your Mastodon account fluctuating. Initially, this might seem perplexing, especially when there’s no apparent reason or notification. Such variations could be attributed to instances disappearing, users deleting their accounts, or in the case of followers, some users opting to unfollow you.

While these numbers might not be of critical importance, especially in the fediverse, understanding the dynamics behind them can be intriguing. And that’s precisely what motivated the creation of the program we’re about to discuss.

The Program Overview

The given program allows you to:

  1. Connect to your Mastodon account.
  2. Retrieve your current list of followers and following.
  3. Compare this list with a previously saved one (from the day before, ideally).
  4. Provide a detailed output of the differences which is formatted in a way that it can be conveniently emailed.

Let’s deep dive into the code and understand its functioning.

import matplotlib
matplotlib.use('Agg')
import requests, json, os, datetime, matplotlib.pyplot as plt, mpld3

Here, essential libraries are imported. Notably:

  • matplotlib for plotting graphical trends.
  • mpld3 to save our plots as interactive HTML files.
  • requests to make API calls to Mastodon.
MASTODON_BASE_URL = "https://mastodon.instance"
ACCESS_TOKEN = xxxx

For the program to access your Mastodon account, you need an ACCESS_TOKEN. This is a critical piece of information that grants the program permission to interact with your account.

Getting the ACCESS_TOKEN:

  1. Log into your Mastodon instance.
  2. Go to Settings > Development > Your applications.
  3. Click on NEW APPLICATION.
  4. Fill out the necessary details and under scopes, grant read permissions.
  5. Once created, you’ll find the Access Token for that application. Use this token in the script.

Moving on, various functions in the program execute tasks like:

  • Fetching user account details (get_current_user_account_id).
  • Retrieving followers and following data (fetch_all_from_endpoint and fetch_mastodon_data).
  • Storing and loading this data (store_data and load_data).
  • Comparing old and new data to identify changes (compare_data).
  • Formatting the comparison results (format_user_list).
  • Saving a daily log of followers/following count (save_daily_log).
  • And generating graphical trends (generate_graphs).

Finally, in the main function, all these operations come together. The program fetches current data, compares it against the old one, prints the differences, stores the new data for future reference, logs the daily counts, and then generates the graphs.

An important feature of the provided script is its capability to visualize your Mastodon follower and following trends over time. This can give you a much more intuitive grasp of the changes in your social media presence, helping you understand patterns that may not be immediately evident from raw numbers.

How the Graphs are Generated

The generate_graphs function is responsible for this visualization:

def generate_graphs():
    ...

Here’s a breakdown:

Loading Data: The function begins by loading your historical follower and following data from the LOG_FILE.

Date Conversion: It then converts the stored date strings into Python’s datetime objects. This conversion is essential to effectively plot the dates on a time axis.

Creating Subplots: Two subplots are initialized using plt.subplots(2, 1, figsize=(10, 12)). This means that the output will be two graphs, one on top of the other, both displayed in a single figure of specified dimensions.

Plotting Followers Trend:

  • The first graph (accessible through axes[0]) plots the follower trend.
  • The x-axis represents dates, while the y-axis shows the number of followers.
  • Blue markers ('o') are used to mark the count on specific dates.
  • The title, labels, and formatting are set appropriately for clarity.

Plotting Following Trend:

  • The second graph (accessible through axes[1]) represents the trend of accounts you’re following.
  • Green markers are used for this graph.
  • Similar to the followers graph, titles, labels, and formatting are adjusted for clarity.

Outputting the Graph: The final step is to save this graphical representation as an interactive HTML using the mpld3.save_html function. This allows you to view the graphs conveniently in a web browser.

By looking at these graphs, you can infer various insights, such as:

Consistent Growth/Decline: A steady upward or downward slope in the followers graph might indicate a consistent growth or loss in popularity respectively.

Sudden Spikes or Dips: These might correlate to specific events or posts. Perhaps a particular toot (post on Mastodon) went viral, leading to a spike in followers? Or maybe a controversial post led to a sudden decline?

Comparing Followers to Following: By placing both graphs together, you can compare your follower trend to your following trend. For instance, if both graphs trend upwards simultaneously, it might indicate a mutual growth in engagement.

While raw numbers give you a snapshot of your current status, trends provide a story of your journey. And as they say, sometimes, it’s not about the destination, but the journey that truly matters.

Understanding the ebb and flow of followers and those you’re following on Mastodon can give insights into the dynamic nature of social networks. While the numbers themselves may not be of paramount importance, tracking them can be a fun and enlightening exercise. This program not only does that but also provides visual feedback through graphs, giving you a more tangible sense of your social presence’s growth or decline. Happy tracking!

The Code

import matplotlib
matplotlib.use('Agg')

import requests
import json
import os
import datetime
import matplotlib.pyplot as plt
import mpld3

# Configuration
MASTODON_BASE_URL = "https://mastodon.instance"
ACCESS_TOKEN = "xxxx"
DATA_FILE = "mastodon_data_full.json"
LOG_FILE = "daily_log.json"


def get_current_user_account_id(access_token):
    headers = {
        "Authorization": f"Bearer {access_token}"
    }
    response = requests.get(f"{MASTODON_BASE_URL}/api/v1/accounts/verify_credentials", headers=headers)
    return response.json()['id']

def fetch_all_from_endpoint(url, access_token):
    headers = {
        "Authorization": f"Bearer {access_token}"
    }
    all_results = []
    next_url = url
    while next_url:
        response = requests.get(next_url, headers=headers)
        all_results.extend(response.json())
        next_url = response.links.get('next', {}).get('url')
    return all_results
def fetch_mastodon_data(account_id, access_token):
    followers_url = f"{MASTODON_BASE_URL}/api/v1/accounts/{account_id}/followers"
    following_url = f"{MASTODON_BASE_URL}/api/v1/accounts/{account_id}/following"
    followers = [{"id": user['id'], "acct": user['acct'], "url": user['url']} for user in fetch_all_from_endpoint(followers_url, access_token)]
    following = [{"id": user['id'], "acct": user['acct'], "url": user['url']} for user in fetch_all_from_endpoint(following_url, access_token)]
    return {
        "followers": followers,
        "following": following
    }

def store_data(data, file_path):
    with open(file_path, 'w') as f:
        json.dump(data, f)

def load_data(file_path):
    if not os.path.exists(file_path):
        return {
            "followers": [],
            "following": []
        }
    with open(file_path, 'r') as f:
        return json.load(f)

def compare_data(old_data, new_data):
    old_followers_ids = {user['id'] for user in old_data['followers']}
    new_followers_ids = {user['id'] for user in new_data['followers']}
    old_following_ids = {user['id'] for user in old_data['following']}
    new_following_ids = {user['id'] for user in new_data['following']}
    new_followers = [user for user in new_data['followers'] if user['id'] not in old_followers_ids]
    lost_followers = [user for user in old_data['followers'] if user['id'] not in new_followers_ids]
    new_following = [user for user in new_data['following'] if user['id'] not in old_following_ids]
    lost_following = [user for user in old_data['following'] if user['id'] not in new_following_ids]
    return {
        "new_followers": new_followers,
        "lost_followers": lost_followers,
        "new_following": new_following,
        "lost_following": lost_following
    }
def format_user_list(users):
    return "\n".join([f"- {user['acct']} ({user['url']})" for user in users])

def save_daily_log(total_followers, total_following):
    today = datetime.date.today().strftime('%Y-%m-%d')
    log_data = {
        'date': today,
        'total_followers': total_followers,
        'total_following': total_following
    }

    if os.path.exists(LOG_FILE):
        with open(LOG_FILE, 'r') as f:
            logs = json.load(f)
    else:
        logs = []

    # Check if there's an entry for today already and update it
    found = False
    for entry in logs:
        if entry['date'] == today:
            entry['total_followers'] = total_followers
            entry['total_following'] = total_following
            found = True
            break

    if not found:
        logs.append(log_data)

    with open(LOG_FILE, 'w') as f:
        json.dump(logs, f, indent=4)

def generate_graphs():
    with open(LOG_FILE, 'r') as f:
        logs = json.load(f)

    # Convert date strings to datetime objects
    dates = [datetime.datetime.strptime(entry['date'], '%Y-%m-%d') for entry in logs]
    followers = [entry['total_followers'] for entry in logs]
    following = [entry['total_following'] for entry in logs]

    fig, axes = plt.subplots(2, 1, figsize=(10, 12))  # Create two subplots

    # Plot Followers
    axes[0].plot(dates, followers, label='Followers', marker='o', color='blue')
    axes[0].set_xticks(dates)
    axes[0].xaxis.set_major_formatter(plt.matplotlib.dates.DateFormatter('%d-%m-%Y'))
    axes[0].set_title('Mastodon Followers Trend')
    axes[0].set_xlabel('Date')
    axes[0].set_ylabel('Count')
    axes[0].legend()
    axes[0].tick_params(axis='x', rotation=45)

    # Plot Following
    axes[1].plot(dates, following, label='Following', marker='o', color='green')
    axes[1].set_xticks(dates)
    axes[1].xaxis.set_major_formatter(plt.matplotlib.dates.DateFormatter('%d-%m-%Y'))
    axes[1].set_title('Mastodon Following Trend')
    axes[1].set_xlabel('Date')
    axes[1].set_ylabel('Count')
    axes[1].legend()
    axes[1].tick_params(axis='x', rotation=45)

    plt.tight_layout()
    mpld3.save_html(fig, "graph.html")

def main():
    account_id = get_current_user_account_id(ACCESS_TOKEN)
    new_data = fetch_mastodon_data(account_id, ACCESS_TOKEN)
    old_data = load_data(DATA_FILE)
    differences = compare_data(old_data, new_data)

    output = []
    output.append("📊 Mastodon Stats 📊")
    output.append(f"\nTotal Followers: {len(new_data['followers'])}")
    output.append(f"Total Following: {len(new_data['following'])}")

    output.append(f"\n📈 New Followers ({len(differences['new_followers'])}) 📈")
    output.append(format_user_list(differences["new_followers"]))

    output.append(f"\n📉 Lost Followers ({len(differences['lost_followers'])}) 📉")
    output.append(format_user_list(differences["lost_followers"]))

    output.append(f"\n📈 New Following ({len(differences['new_following'])}) 📈")
    output.append(format_user_list(differences["new_following"]))

    output.append(f"\n📉 Lost Following ({len(differences['lost_following'])}) 📉")
    output.append(format_user_list(differences["lost_following"]))

    print("\n".join(output))
    store_data(new_data, DATA_FILE)
    save_daily_log(len(new_data['followers']), len(new_data['following']))
    generate_graphs()

if __name__ == "__main__":
    main()

See also