diff --git a/daemon.py b/daemon.py index 8587b90ec..c1104abbd 100644 --- a/daemon.py +++ b/daemon.py @@ -119,6 +119,7 @@ from inbox import run_inbox_queue_watchdog from inbox import save_post_to_inbox_queue from inbox import populate_replies from inbox import receive_edit_to_post +from follow import get_followers_for_domain from follow import follower_approval_active from follow import is_following_actor from follow import get_following_feed @@ -876,6 +877,7 @@ class PubServer(BaseHTTPRequestHandler): def _secure_mode(self, curr_session, proxy_type: str, force: bool = False) -> bool: """http authentication of GET requests for json + aka authorized fetch """ if not self.server.secure_mode and not force: return True @@ -16901,11 +16903,63 @@ class PubServer(BaseHTTPRequestHandler): '_GET', '_security_txt[calling_domain]', self.server.debug) + # followers synchronization if self.path.startswith('/users/') and \ self.path.endswith('/followers_synchronization'): - print('DEBUG: followers synchronization request ' + - self.path + ' ' + calling_domain) + if self.server.followers_synchronization: + # only do one request at a time + self._503() + return + self.server.followers_synchronization = True + if self.server.debug: + print('DEBUG: followers synchronization request ' + + self.path + ' ' + calling_domain) + # check authorized fetch + if self._secure_mode(curr_session, proxy_type): + nickname = get_nickname_from_actor(self.path) + sync_list = \ + get_followers_for_domain(self.server.base_dir, + nickname, self.server.domain, + calling_domain) + id_str = self.server.http_prefix + '://' + \ + self.server.domain_full + \ + self.path.replace('/followers_synchronization', + '/followers?domain=' + calling_domain) + sync_json = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': id_str, + 'orderedItems': sync_list, + 'type': 'OrderedCollection' + } + msg_str = json.dumps(sync_json, ensure_ascii=False) + msg_str = self._convert_domains(calling_domain, referer_domain, + msg_str) + msg = msg_str.encode('utf-8') + msglen = len(msg) + self._set_headers('application/json', msglen, + None, calling_domain, False) + self._write(msg) + self.server.followers_synchronization = False + return + else: + # request was not signed + result_json = { + "error": "Request not signed" + } + msg_str = json.dumps(result_json, ensure_ascii=False) + msg = msg_str.encode('utf-8') + msglen = len(msg) + accept_str = self.headers['Accept'] + if 'json' in accept_str: + protocol_str = \ + get_json_content_from_accept(accept_str) + self._set_headers(protocol_str, msglen, + None, calling_domain, False) + self._write(msg) + self.server.followers_synchronization = False + return self._404() + self.server.followers_synchronization = False return if self.path == '/logout': @@ -23333,6 +23387,9 @@ def run_daemon(max_hashtags: int, # scan the theme directory for any svg files containing scripts assert not scan_themes_for_scripts(base_dir) + # lock for followers synchronization + httpd.followers_synchronization = False + # permitted sites from which the buy button may be displayed httpd.buy_sites = load_buy_sites(base_dir) diff --git a/follow.py b/follow.py index c7934dc3f..51d4800ed 100644 --- a/follow.py +++ b/follow.py @@ -9,6 +9,7 @@ __module_group__ = "ActivityPub" from pprint import pprint import os +from utils import get_user_paths from utils import acct_handle_dir from utils import has_object_string_object from utils import has_object_string_type @@ -1360,6 +1361,50 @@ def deny_follow_request_via_server(session, return deny_html +def get_followers_for_domain(base_dir: str, + nickname: str, domain: str, + search_domain: str) -> []: + """Returns the followers for a given domain + this is used for followers synchronization + """ + followers_filename = \ + acct_dir(base_dir, nickname, domain) + '/followers.txt' + if not os.path.isfile(followers_filename): + return [] + lines = [] + try: + with open(followers_filename, 'r', encoding='utf-8') as fp_foll: + lines = fp_foll.read().splitlines() + except OSError: + print('EX: get_followers_for_domain unable to read followers ' + + followers_filename) + result = [] + for line_str in lines: + if search_domain not in line_str: + continue + if line_str.endswith('@' + search_domain): + nick = line_str.split('@')[0] + paths_list = get_user_paths() + found = False + for prefix in ('https', 'http'): + if found: + break + for possible_path in paths_list: + url = prefix + '://' + search_domain + \ + possible_path + nick + filename = base_dir + '/cache/actors/' + \ + url.replace('/', '#') + '.json' + if not os.path.isfile(filename): + continue + if url not in result: + result.append(url) + found = True + break + elif '://' + search_domain in line_str: + result.append(line_str) + return result + + def get_followers_of_actor(base_dir: str, actor: str, debug: bool) -> {}: """In a shared inbox if we receive a post we know who it's from and if it's addressed to followers then we need to get a list of those.