Getting Replies Feature
Published: 2024-08-09 16:46:50 +1000 +1000

Given there exists a mutual relationship between your static site actor, and other ActivityPub platform user. Many ActivityPub platforms send out a HTTP POST request to your actor inbox, whenever the other actor publish new content that is relevant to you. Because our site is static by nature, we will not be able to actively listen for the requests and take appropriate action. Despite this, there are alternative measures we can take to solve this problems. For our case, we use the outbox of our list of followers and following, in followers.json & following.json respectively, make a HTTP GET request to retrieve a list of post and filter them out accordingly.

Here is an example code:

import requests
import json

from utility import get_follower_url, extract_followers_outbox, ActivityDTO

class PostReplyHandler:
    def __init__(self, actor_id, webfinger):
        self.actor_id = actor_id
        self.domain, self.username = webfinger.split("@")[1:]

    def get_replies(self):
        # Getting a list of followers
        activity_dto = ActivityDTO()
        follower_url = get_follower_url(self.actor_id)
        extract_followers_outbox(follower_url, activity_dto)
        
        filtered_posts = []
        for follower in activity_dto.followers:
            outbox_url  = follower[1]
            posts = self.__get_posts(outbox_url)
            filtered_posts.append(self.__filtered_post(posts))
        return filtered_posts
    
    def __get_posts(self, outbox_url):
        headers = { "accept": "application/activity+json" } 
        response = requests.get(outbox_url, headers=headers)
        
        if not response.ok: 
            return []
        
        data = json.loads(response.text)
        if 'orderedItems' in data.keys():
            return self.__get_post_from_ordered_items(data)
        elif "first" in data.keys():
            first = data['first']
            if isinstance(first, dict):
                return self.__get_post_from_ordered_items(first)
                
        return self.__get_post_from_ext_urls(data)
         
    def __get_post_from_ordered_items(self, data): 
            items = data['orderedItems']
            posts = []
            urls = []
            for item in items: 
                if type(item) == str:
                    # This couldbe urls leading to more problems
                    urls.append(item)

                elif type(item) == dict: 
                    posts.append(json.loads(item))
                    
            for url in urls:
                headers = { "accept": "application/activity+json"}
                response = requests.get(url, headers=headers)
                if not response.ok:
                    continue
                posts.append(json.loads(response.text))
            
            if "next" in data.keys():
                posts += self.__get_post_from_ext_urls(data)
            return posts
            
    def __get_post_from_ext_urls(self, data):
        excluded_keys = [ "@context", "id", "type", "totalItems", "orderedItems"] 
        urls = []
        # Getting relevant urls
        for key in data:
            if key not in excluded_keys:
                urls.append(data[key])
        
        posts = [] 
        while len(urls) != 0:
            url = urls.pop(0)
            if isinstance(url, dict):
                for data in url['orderedItems']:
                    if isinstance(data, str):
                        urls.insert(0, data)
                    else:
                        posts.append(data)
                continue
            response = requests.get(url, headers={ "accept": "application/activity+json"})
            json_data = json.loads(response.text)
            
            if "orderedItems" not in json_data.keys(): 
                break 
            
            for item in json_data['orderedItems']:
                if type(item) == str:
                    urls.append(item)
                elif type(item) == dict: 
                    posts.append(item)

            # Registered the next urls
            if "next" in json_data.keys():
                urls.append(json_data["next"])
            elif "last" in json_data.keys():
                urls.append(json_data["last"])
            else: 
                break
        return posts

    def __filtered_post(self, posts): 
        filtered_posts = []
        for post in posts:
            if 'object' not in post.keys(): 
                continue
            elif post['type'] != "Create":
                continue

            post_obj = post['object']
            if 'inReplyTo' not in post_obj.keys(): 
                continue            
            elif post_obj['inReplyTo'] == None: 
                continue
        
            in_reply_to = post_obj['inReplyTo']
            if self.domain in in_reply_to and self.username in in_reply_to:
                filtered_posts.append(post)
            
        return filtered_posts

if __name__ == "__main__":
    actor_id =  "https://noah.netlify.app/noah/actor.json"

    webfinger = "@noah@netlify.app"
    handler = PostReplyHandler(actor_id, webfinger)
    print(handler.get_replies())

See also