diff --git a/blocking.py b/blocking.py index 3f40d1884..39dbd3976 100644 --- a/blocking.py +++ b/blocking.py @@ -910,6 +910,9 @@ def mute_post(base_dir: str, nickname: str, domain: str, port: int, if post_json_obj.get('conversation'): mute_conversation(base_dir, nickname, domain, post_json_obj['conversation']) + elif post_json_obj.get('context'): + mute_conversation(base_dir, nickname, domain, + post_json_obj['context']) # does this post have ignores on it from differenent actors? if not post_json_obj.get('ignores'): @@ -1048,6 +1051,9 @@ def unmute_post(base_dir: str, nickname: str, domain: str, port: int, if post_json_obj.get('conversation'): unmute_conversation(base_dir, nickname, domain, post_json_obj['conversation']) + elif post_json_obj.get('context'): + unmute_conversation(base_dir, nickname, domain, + post_json_obj['context']) if post_json_obj.get('ignores'): domain_full = get_full_domain(domain, port) diff --git a/conversation.py b/conversation.py index 689ac7269..9945eb22d 100644 --- a/conversation.py +++ b/conversation.py @@ -25,14 +25,18 @@ def _get_conversation_filename(base_dir: str, nickname: str, domain: str, """ if not has_object_dict(post_json_object): return None - if not post_json_object['object'].get('conversation'): + if not post_json_object['object'].get('conversation') and \ + not post_json_object['object'].get('context'): return None if not post_json_object['object'].get('id'): return None conversation_dir = acct_dir(base_dir, nickname, domain) + '/conversation' if not os.path.isdir(conversation_dir): os.mkdir(conversation_dir) - conversation_id = post_json_object['object']['conversation'] + if post_json_object['object'].get('conversation'): + conversation_id = post_json_object['object']['conversation'] + else: + conversation_id = post_json_object['object']['context'] conversation_id = conversation_id.replace('/', '#') return conversation_dir + '/' + conversation_id diff --git a/daemon.py b/daemon.py index 1ea66fdf2..b8f69fecd 100644 --- a/daemon.py +++ b/daemon.py @@ -30,6 +30,7 @@ from session import set_session_for_sender from webfinger import webfinger_meta from webfinger import webfinger_node_info from webfinger import webfinger_lookup +from webfinger import wellknown_protocol_handler from webfinger import webfinger_update from mastoapiv1 import masto_api_v1_response from metadata import meta_data_node_info @@ -277,6 +278,7 @@ from languages import set_actor_languages from languages import get_understood_languages from like import update_likes_collection from reaction import update_reaction_collection +from utils import is_public_post_from_url from utils import license_link_from_name from utils import acct_handle_dir from utils import load_reverse_timeline @@ -1115,7 +1117,8 @@ class PubServer(BaseHTTPRequestHandler): return False def _redirect_headers(self, redirect: str, cookie: str, - calling_domain: str) -> None: + calling_domain: str, + code: int = 303) -> None: if '://' not in redirect: if calling_domain.endswith('.onion') and self.server.onion_domain: redirect = 'http://' + self.server.onion_domain + redirect @@ -1128,7 +1131,10 @@ class PubServer(BaseHTTPRequestHandler): print('WARN: redirect was not an absolute url, changed to ' + redirect) - self.send_response(303) + self.send_response(code) + + if code != 303: + print('Redirect headers: ' + str(code)) if cookie: cookie_str = cookie.replace('SET:', '').strip() @@ -1685,7 +1691,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.security_txt_is_active = False return True - def _webfinger(self, calling_domain: str, referer_domain: str) -> bool: + def _webfinger(self, calling_domain: str, referer_domain: str, + cookie: str) -> bool: if not self.path.startswith('/.well-known'): return False if self.server.debug: @@ -1722,6 +1729,29 @@ class PubServer(BaseHTTPRequestHandler): self.path.startswith('/friendi'): self._404() return True + # protocol handler. See https://fedi-to.github.io/protocol-handler.html + if self.path.startswith('/.well-known/protocol-handler'): + if calling_domain.endswith('.onion'): + protocol_url = \ + wellknown_protocol_handler(self.path, + self.server.base_dir, 'http', + self.server.onion_domain) + elif calling_domain.endswith('.i2p'): + protocol_url = \ + wellknown_protocol_handler(self.path, self.server.base_dir, + 'http', self.server.i2p_domain) + else: + protocol_url = \ + wellknown_protocol_handler(self.path, self.server.base_dir, + self.server.http_prefix, + self.server.domain_full) + if protocol_url: + self._redirect_headers(protocol_url, cookie, + calling_domain, 308) + else: + self._404() + return True + # nodeinfo if self.path.startswith('/.well-known/nodeinfo') or \ self.path.startswith('/.well-known/x-nodeinfo'): if calling_domain.endswith('.onion') and \ @@ -11588,10 +11618,10 @@ class PubServer(BaseHTTPRequestHandler): # get the replies file post_dir = \ acct_dir(base_dir, nickname, domain) + '/' + boxname + orig_post_url = http_prefix + ':##' + domain_full + '#users#' + \ + nickname + '#statuses#' + status_number post_replies_filename = \ - post_dir + '/' + \ - http_prefix + ':##' + domain_full + '#users#' + \ - nickname + '#statuses#' + status_number + '.replies' + post_dir + '/' + orig_post_url + '.replies' if not os.path.isfile(post_replies_filename): # There are no replies, # so show empty collection @@ -11720,6 +11750,13 @@ class PubServer(BaseHTTPRequestHandler): 'type': 'OrderedCollectionPage' } + # if the original post is public then return the replies + replies_are_public = \ + is_public_post_from_url(base_dir, nickname, domain, + orig_post_url) + if replies_are_public: + authorized = True + # populate the items list with replies populate_replies_json(base_dir, nickname, domain, post_replies_filename, @@ -18042,6 +18079,7 @@ class PubServer(BaseHTTPRequestHandler): if html_getreq and self.path != '/login' and \ not is_image_file(self.path) and \ self.path != '/' and \ + not self.path.startswith('/.well-known/protocol-handler') and \ self.path != '/users/news/linksmobile' and \ self.path != '/users/news/newswiremobile': if self._redirect_to_login_screen(calling_domain, self.path, @@ -18378,7 +18416,7 @@ class PubServer(BaseHTTPRequestHandler): return # get webfinger endpoint for a person - if self._webfinger(calling_domain, referer_domain): + if self._webfinger(calling_domain, referer_domain, cookie): fitness_performance(getreq_start_time, self.server.fitness, '_GET', 'webfinger called', self.server.debug) diff --git a/desktop_client.py b/desktop_client.py index a2d4cad96..e7d4f6de6 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -1825,6 +1825,9 @@ def run_desktop_client(base_dir: str, proxy_type: str, http_prefix: str, if post_json_object['object'].get('conversation'): conversation_id = \ post_json_object['object']['conversation'] + elif post_json_object['object'].get('context'): + conversation_id = \ + post_json_object['object']['context'] session_reply = create_session(proxy_type) _desktop_reply_to_post(session_reply, post_id, base_dir, nickname, diff --git a/happening.py b/happening.py index 1d479cfe9..7d14f323d 100644 --- a/happening.py +++ b/happening.py @@ -911,6 +911,7 @@ def _dav_store_event(base_dir: str, nickname: str, domain: str, "object": { "id": post_id, "conversation": post_id, + "context": post_id, "type": "Note", "summary": None, "inReplyTo": None, diff --git a/inbox.py b/inbox.py index fff698f05..0986f627e 100644 --- a/inbox.py +++ b/inbox.py @@ -3993,6 +3993,8 @@ def _create_reply_notification_file(base_dir: str, nickname: str, domain: str, conversation_id = None if post_json_object['object'].get('conversation'): conversation_id = post_json_object['object']['conversation'] + elif post_json_object['object'].get('context'): + conversation_id = post_json_object['object']['context'] if not post_json_object['object'].get('inReplyTo'): return is_reply_to_muted_post diff --git a/posts.py b/posts.py index f1b0b13f6..21a8c0574 100644 --- a/posts.py +++ b/posts.py @@ -1126,6 +1126,7 @@ def _create_post_s2s(base_dir: str, nickname: str, domain: str, port: int, 'object': { 'id': new_post_id, 'conversation': conversation_id, + 'context': conversation_id, 'type': post_object_type, 'summary': summary, 'inReplyTo': in_reply_to, @@ -1193,6 +1194,7 @@ def _create_post_c2s(base_dir: str, nickname: str, domain: str, port: int, "@context": post_context, 'id': new_post_id, 'conversation': conversation_id, + 'context': conversation_id, 'type': post_object_type, 'summary': summary, 'inReplyTo': in_reply_to, diff --git a/session.py b/session.py index f71df18d3..f60522444 100644 --- a/session.py +++ b/session.py @@ -21,6 +21,8 @@ from http.client import HTTPConnection def create_session(proxy_type: str): + """ Creates a new session + """ session = None try: session = requests.session() @@ -226,6 +228,8 @@ def get_json(signing_priv_key_pem: str, version: str = __version__, http_prefix: str = 'https', domain: str = 'testdomain', timeout_sec: int = 20, quiet: bool = False) -> {}: + """Download some json + """ if not isinstance(url, str): if debug and not quiet: print('url: ' + str(url)) @@ -263,6 +267,8 @@ def get_vcard(xml_format: bool, session, url: str, params: {}, debug: bool, version: str, http_prefix: str, domain: str, timeout_sec: int = 20, quiet: bool = False) -> {}: + """Download a vcard + """ if not isinstance(url, str): if debug and not quiet: print('url: ' + str(url)) @@ -341,6 +347,8 @@ def download_html(signing_priv_key_pem: str, session, url: str, headers: {}, params: {}, debug: bool, version: str, http_prefix: str, domain: str, timeout_sec: int = 20, quiet: bool = False) -> {}: + """Download a html document + """ if not isinstance(url, str): if debug and not quiet: print('url: ' + str(url)) @@ -473,6 +481,8 @@ def download_ssml(signing_priv_key_pem: str, session, url: str, headers: {}, params: {}, debug: bool, version: str, http_prefix: str, domain: str, timeout_sec: int = 20, quiet: bool = False) -> {}: + """Download a ssml document + """ if not isinstance(url, str): if debug and not quiet: print('url: ' + str(url)) diff --git a/utils.py b/utils.py index a91995a71..80fb52d7b 100644 --- a/utils.py +++ b/utils.py @@ -2032,13 +2032,17 @@ def _delete_conversation_post(base_dir: str, nickname: str, domain: str, """ if not has_object_dict(post_json_object): return False - if not post_json_object['object'].get('conversation'): + if not post_json_object['object'].get('conversation') and \ + not post_json_object['object'].get('context'): return False if not post_json_object['object'].get('id'): return False conversation_dir = \ acct_dir(base_dir, nickname, domain) + '/conversation' - conversation_id = post_json_object['object']['conversation'] + if post_json_object['object'].get('conversation'): + conversation_id = post_json_object['object']['conversation'] + else: + conversation_id = post_json_object['object']['context'] conversation_id = conversation_id.replace('/', '#') post_id = post_json_object['object']['id'] conversation_filename = conversation_dir + '/' + conversation_id diff --git a/video.py b/video.py index 3a67e1de1..8e764ad76 100644 --- a/video.py +++ b/video.py @@ -150,6 +150,7 @@ def convert_video_to_note(base_dir: str, nickname: str, domain: str, 'object': { 'id': new_post_id, 'conversation': conversation_id, + 'context': conversation_id, 'type': 'Note', 'summary': None, 'inReplyTo': None, diff --git a/webapp_create_post.py b/webapp_create_post.py index 9df6413ca..8155b6c96 100644 --- a/webapp_create_post.py +++ b/webapp_create_post.py @@ -258,6 +258,8 @@ def html_new_post(edit_post_params: {}, return '' if edited_post_json['object'].get('conversation'): conversation_id = edited_post_json['object']['conversation'] + elif edited_post_json['object'].get('context'): + conversation_id = edited_post_json['object']['context'] if edit_post_params.get('replyTo'): in_reply_to = edit_post_params['replyTo'] if edit_post_params['scope'] == 'dm': diff --git a/webapp_post.py b/webapp_post.py index fc6da176d..31c8ac4da 100644 --- a/webapp_post.py +++ b/webapp_post.py @@ -2177,10 +2177,13 @@ def individual_post_as_html(signing_priv_key_pem: str, comments_enabled = False conversation_id = None - if isinstance(post_json_object['object'], dict) and \ - 'conversation' in post_json_object['object']: - if post_json_object['object']['conversation']: - conversation_id = post_json_object['object']['conversation'] + if isinstance(post_json_object['object'], dict): + if 'conversation' in post_json_object['object']: + if post_json_object['object']['conversation']: + conversation_id = post_json_object['object']['conversation'] + elif 'context' in post_json_object['object']: + if post_json_object['object']['context']: + conversation_id = post_json_object['object']['context'] public_reply = False unlisted_reply = False diff --git a/webfinger.py b/webfinger.py index 7235ed909..257147862 100644 --- a/webfinger.py +++ b/webfinger.py @@ -252,6 +252,41 @@ def webfinger_meta(http_prefix: str, domain_full: str) -> str: return meta_str +def wellknown_protocol_handler(path: str, base_dir: str, + http_prefix: str, domain_full: str) -> {}: + """See https://fedi-to.github.io/protocol-handler.html + """ + if not path.startswith('/.well-known/protocol-handler?'): + return None + + if 'target=' in path: + path = urllib.parse.unquote(path) + target = path.split('target=')[1] + if ';' in target: + target = target.split(';')[0] + if not target: + return None + if not target.startswith('web+epicyon:') and \ + not target.startswith('web+mastodon:') and \ + not target.startswith('web+ap:'): + return None + handle = target.split(':', 1)[1].strip() + if handle.startswith('//'): + handle = handle[2:] + if handle.startswith('@'): + handle = handle[1:] + if '@' in handle: + nickname = handle.split('@')[0] + domain = handle.split('@')[1] + else: + nickname = handle + domain = domain_full + # not an open redirect + if domain == domain_full: + return http_prefix + '://' + domain_full + '/users/' + nickname + return None + + def webfinger_lookup(path: str, base_dir: str, domain: str, onion_domain: str, i2p_domain: str, port: int, debug: bool) -> {}: