diff --git a/daemon.py b/daemon.py index 0e8ee384b..1a53f123a 100644 --- a/daemon.py +++ b/daemon.py @@ -8,6 +8,7 @@ __status__ = "Production" __module_group__ = "Core" from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer, HTTPServer +import copy import sys import json import time @@ -2362,7 +2363,8 @@ class PubServer(BaseHTTPRequestHandler): nickname, domain, post_filename, debug, - self.server.recent_posts_cache) + self.server.recent_posts_cache, + True) if nickname != 'news': # if this is a local blog post then also remove it # from the news actor @@ -2378,7 +2380,8 @@ class PubServer(BaseHTTPRequestHandler): 'news', domain, post_filename, debug, - self.server.recent_posts_cache) + self.server.recent_posts_cache, + True) self._redirect_headers(actor_str + '/moderation', cookie, calling_domain) @@ -8654,7 +8657,7 @@ class PubServer(BaseHTTPRequestHandler): if self.server.iconsCache.get('repeat_inactive.png'): del self.server.iconsCache['repeat_inactive.png'] - # delete the announce post + # delete the announce post if '?unannounce=' in path: announce_url = path.split('?unannounce=')[1] if '?' in announce_url: @@ -8668,7 +8671,7 @@ class PubServer(BaseHTTPRequestHandler): if post_filename: delete_post(base_dir, http_prefix, nickname, domain, post_filename, - debug, recent_posts_cache) + debug, recent_posts_cache, True) self._post_to_outbox(new_undo_announce, self.server.project_version, @@ -19398,12 +19401,12 @@ class PubServer(BaseHTTPRequestHandler): np_thread.kill() # make a copy of self.headers - headers = {} - headers_without_cookie = {} - for dict_entry_name, header_line in self.headers.items(): - headers[dict_entry_name] = header_line - if dict_entry_name.lower() != 'cookie': - headers_without_cookie[dict_entry_name] = header_line + headers = copy.deepcopy(self.headers) + headers_without_cookie = copy.deepcopy(headers) + if 'cookie' in headers_without_cookie: + del headers_without_cookie['cookie'] + if 'Cookie' in headers_without_cookie: + del headers_without_cookie['Cookie'] print('New post headers: ' + str(headers_without_cookie)) length = int(headers['Content-Length']) diff --git a/delete.py b/delete.py index 6713ceec5..00e83d33a 100644 --- a/delete.py +++ b/delete.py @@ -170,7 +170,7 @@ def outbox_delete(base_dir: str, http_prefix: str, print(message_id) return True delete_post(base_dir, http_prefix, delete_nickname, delete_domain, - post_filename, debug, recent_posts_cache) + post_filename, debug, recent_posts_cache, True) if debug: print('DEBUG: post deleted via c2s - ' + post_filename) diff --git a/epicyon-blog.css b/epicyon-blog.css index 67acfbf67..74498287d 100644 --- a/epicyon-blog.css +++ b/epicyon-blog.css @@ -581,13 +581,16 @@ input[type=submit]:hover { } .timeline-avatar { - margin: 10px auto; + margin: 0px 0px; padding: 0px 0px; + width: 8%; + float: left; } .timeline-avatar-reply { padding: 0px 0px; - width: 80%; + width: 6%; + float: left; } .search-result-text { @@ -795,11 +798,15 @@ div.gallery img { } .timeline-avatar img { opacity: 1.0; - width: 8%; - height: 8%; + width: 100%; padding: 0px 0px; - -ms-transform: translateY(-10%); - transform: translateY(-10%); + border-radius: var(--avatar-rounding); + } + .timeline-avatar-reply img { + opacity: 1.0; + width: 80%; + padding: 0px 0px; + margin: 0px 10%; border-radius: var(--avatar-rounding); } .cwButton { @@ -1149,13 +1156,28 @@ div.gallery img { margin: 1% 3%; border-radius: 0%; } + .timeline-avatar { + margin: 0px 0px; + padding: 0px 0px; + width: 15%; + float: left; + } + .timeline-avatar-reply { + padding: 0px 0px; + width: 12%; + float: left; + } .timeline-avatar img { opacity: 1.0; - width: 15%; - height: 15%; + width: 100%; padding: 0px 0px; - -ms-transform: translateY(-10%); - transform: translateY(-10%); + border-radius: var(--avatar-rounding); + } + .timeline-avatar-reply img { + opacity: 1.0; + width: 80%; + padding: 0px 0px; + margin: 0px 10%; border-radius: var(--avatar-rounding); } .cwButton { @@ -1496,13 +1518,28 @@ div.gallery img { margin: 1% 3%; border-radius: 0%; } + .timeline-avatar { + margin: 0px 0px; + padding: 0px 0px; + width: 15%; + float: left; + } + .timeline-avatar-reply { + padding: 0px 0px; + width: 12%; + float: left; + } .timeline-avatar img { opacity: 1.0; - width: 15%; - height: 15%; + width: 100%; padding: 0px 0px; - -ms-transform: translateY(-10%); - transform: translateY(-10%); + border-radius: var(--avatar-rounding); + } + .timeline-avatar-reply img { + opacity: 1.0; + width: 80%; + padding: 0px 0px; + margin: 0px 10%; border-radius: var(--avatar-rounding); } .cwButton { diff --git a/epicyon-links.css b/epicyon-links.css index a45a1e4c6..e0eb5ae89 100644 --- a/epicyon-links.css +++ b/epicyon-links.css @@ -582,8 +582,10 @@ input[type=submit]:hover { } .timeline-avatar { - margin: 10px auto; + margin: 0px 0px; padding: 0px 0px; + width: 8%; + float: left; } .timeline-avatar:hover { @@ -592,7 +594,8 @@ input[type=submit]:hover { .timeline-avatar-reply { padding: 0px 0px; - width: 80%; + width: 6%; + float: left; } .search-result-text { @@ -1054,11 +1057,15 @@ aside .toggle-inside li { } .timeline-avatar img { opacity: 1.0; - width: 8%; - height: 8%; + width: 100%; padding: 0px 0px; - -ms-transform: translateY(-10%); - transform: translateY(-10%); + border-radius: var(--avatar-rounding); + } + .timeline-avatar-reply img { + opacity: 1.0; + width: 80%; + padding: 0px 0px; + margin: 0px 10%; border-radius: var(--avatar-rounding); } .buttonevent { @@ -1523,13 +1530,28 @@ aside .toggle-inside li { margin: 1% 3%; border-radius: 0%; } + .timeline-avatar { + margin: 0px 0px; + padding: 0px 0px; + width: 15%; + float: left; + } + .timeline-avatar-reply { + padding: 0px 0px; + width: 12%; + float: left; + } .timeline-avatar img { opacity: 1.0; - width: 15%; - height: 15%; + width: 100%; padding: 0px 0px; - -ms-transform: translateY(-10%); - transform: translateY(-10%); + border-radius: var(--avatar-rounding); + } + .timeline-avatar-reply img { + opacity: 1.0; + width: 80%; + padding: 0px 0px; + margin: 0px 10%; border-radius: var(--avatar-rounding); } .buttonevent { @@ -1986,13 +2008,28 @@ aside .toggle-inside li { margin: 1% 3%; border-radius: 0%; } + .timeline-avatar { + margin: 0px 0px; + padding: 0px 0px; + width: 15%; + float: left; + } + .timeline-avatar-reply { + padding: 0px 0px; + width: 12%; + float: left; + } .timeline-avatar img { opacity: 1.0; - width: 15%; - height: 15%; + width: 100%; padding: 0px 0px; - -ms-transform: translateY(-10%); - transform: translateY(-10%); + border-radius: var(--avatar-rounding); + } + .timeline-avatar-reply img { + opacity: 1.0; + width: 80%; + padding: 0px 0px; + margin: 0px 10%; border-radius: var(--avatar-rounding); } .buttonevent { diff --git a/epicyon-profile.css b/epicyon-profile.css index 6269c9fd0..746897e81 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -885,8 +885,10 @@ input[type=submit]:hover { } .timeline-avatar { - margin: 10px auto; + margin: 0px 0px; padding: 0px 0px; + width: 8%; + float: left; } .timeline-avatar:hover { @@ -895,7 +897,8 @@ input[type=submit]:hover { .timeline-avatar-reply { padding: 0px 0px; - width: 80%; + width: 6%; + float: left; } .search-result-text { @@ -1473,11 +1476,15 @@ h3 { } .timeline-avatar img { opacity: 1.0; - width: 8%; - height: 8%; + width: 100%; padding: 0px 0px; - -ms-transform: translateY(-10%); - transform: translateY(-10%); + border-radius: var(--avatar-rounding); + } + .timeline-avatar-reply img { + opacity: 1.0; + width: 80%; + padding: 0px 0px; + margin: 0px 10%; border-radius: var(--avatar-rounding); } .buttonevent { @@ -2228,13 +2235,28 @@ h3 { margin-right: 0px; border-radius: 0%; } + .timeline-avatar { + margin: 0px 0px; + padding: 0px 0px; + width: 15%; + float: left; + } + .timeline-avatar-reply { + padding: 0px 0px; + width: 12%; + float: left; + } .timeline-avatar img { opacity: 1.0; - width: 15%; - height: 15%; + width: 100%; padding: 0px 0px; - -ms-transform: translateY(-10%); - transform: translateY(-10%); + border-radius: var(--avatar-rounding); + } + .timeline-avatar-reply img { + opacity: 1.0; + width: 80%; + padding: 0px 0px; + margin: 0px 10%; border-radius: var(--avatar-rounding); } .buttonevent { @@ -2988,13 +3010,28 @@ h3 { margin-right: 0px; border-radius: 0%; } + .timeline-avatar { + margin: 0px 0px; + padding: 0px 0px; + width: 15%; + float: left; + } + .timeline-avatar-reply { + padding: 0px 0px; + width: 12%; + float: left; + } .timeline-avatar img { opacity: 1.0; - width: 15%; - height: 15%; + width: 100%; padding: 0px 0px; - -ms-transform: translateY(-10%); - transform: translateY(-10%); + border-radius: var(--avatar-rounding); + } + .timeline-avatar-reply img { + opacity: 1.0; + width: 80%; + padding: 0px 0px; + margin: 0px 10%; border-radius: var(--avatar-rounding); } .buttonevent { diff --git a/happening.py b/happening.py index 39bfd350c..bf2ede6b8 100644 --- a/happening.py +++ b/happening.py @@ -1276,7 +1276,7 @@ def dav_delete_response(base_dir: str, nickname: str, domain: str, token_post_id) delete_post(base_dir, http_prefix, nickname, domain, post_filename, - debug, recent_posts_cache) + debug, recent_posts_cache, True) return 'Ok' diff --git a/inbox.py b/inbox.py index bef69c55c..ef6498d68 100644 --- a/inbox.py +++ b/inbox.py @@ -2058,7 +2058,7 @@ def _receive_delete(session, handle: str, is_group: bool, base_dir: str, return True delete_post(base_dir, http_prefix, handle_nickname, handle_domain, post_filename, debug, - recent_posts_cache) + recent_posts_cache, True) if debug: print('DEBUG: post deleted - ' + post_filename) @@ -2069,7 +2069,7 @@ def _receive_delete(session, handle: str, is_group: bool, base_dir: str, if post_filename: delete_post(base_dir, http_prefix, 'news', handle_domain, post_filename, debug, - recent_posts_cache) + recent_posts_cache, True) if debug: print('DEBUG: blog post deleted - ' + post_filename) return True @@ -4094,7 +4094,7 @@ def _inbox_after_initial(server, inbox_start_time, if edited_filename != destination_filename: delete_post(base_dir, http_prefix, nickname, domain, edited_filename, - debug, recent_posts_cache) + debug, recent_posts_cache, True) # update the indexes for different timelines for boxname in update_index_list: diff --git a/posts.py b/posts.py index f21f76cdb..1780999d7 100644 --- a/posts.py +++ b/posts.py @@ -4294,7 +4294,7 @@ def archive_posts_for_person(http_prefix: str, nickname: str, domain: str, '.json.' + ext)) else: delete_post(base_dir, http_prefix, nickname, domain, - file_path, False, recent_posts_cache) + file_path, False, recent_posts_cache, False) # remove cached html posts post_cache_filename = \ diff --git a/tests.py b/tests.py index d6c30c06c..7d4e60811 100644 --- a/tests.py +++ b/tests.py @@ -3393,11 +3393,14 @@ def test_client_to_server(base_dir: str): inbox_path = bob_dir + '/accounts/bob@' + bob_domain + '/inbox' outbox_path = alice_dir + '/accounts/alice@' + alice_domain + '/outbox' - posts_before = \ + bob_posts_before = \ len([name for name in os.listdir(inbox_path) if os.path.isfile(os.path.join(inbox_path, name))]) + alice_posts_before = \ + len([name for name in os.listdir(outbox_path) + if os.path.isfile(os.path.join(outbox_path, name))]) print('\n\nEVENT: Alice deletes her post: ' + outbox_post_id + ' ' + - str(posts_before)) + str(alice_posts_before)) password = 'alicepass' send_delete_via_server(alice_dir, session_alice, 'alice', password, alice_domain, alice_port, @@ -3408,14 +3411,20 @@ def test_client_to_server(base_dir: str): if os.path.isdir(inbox_path): test = len([name for name in os.listdir(inbox_path) if os.path.isfile(os.path.join(inbox_path, name))]) - if test == posts_before-1: + if test == bob_posts_before-1: break time.sleep(1) test = len([name for name in os.listdir(inbox_path) if os.path.isfile(os.path.join(inbox_path, name))]) - assert test == posts_before - 1 - print(">>> post deleted from Alice's outbox and Bob's inbox") + assert test == bob_posts_before - 1 + print(">>> post was deleted from Bob's inbox") + test = len([name for name in os.listdir(outbox_path) + if os.path.isfile(os.path.join(outbox_path, name))]) + # this should be unchanged because a delete post was added + # at the outbox and one was removed + assert test == alice_posts_before + print(">>> post deleted from Alice's outbox") assert valid_inbox(bob_dir, 'bob', bob_domain) assert valid_inbox_filenames(bob_dir, 'bob', bob_domain, alice_domain, alice_port) diff --git a/utils.py b/utils.py index 3c704fd9f..f5a49d9b4 100644 --- a/utils.py +++ b/utils.py @@ -91,6 +91,15 @@ def get_actor_languages_list(actor_json: {}) -> []: return [] +def has_object_dict(post_json_object: {}) -> bool: + """Returns true if the given post has an object dict + """ + if post_json_object.get('object'): + if isinstance(post_json_object['object'], dict): + return True + return False + + def get_content_from_post(post_json_object: {}, system_language: str, languages_understood: [], contentType: str = "content") -> str: @@ -1610,7 +1619,8 @@ def _is_reply_to_blog_post(base_dir: str, nickname: str, domain: str, def _delete_post_remove_replies(base_dir: str, nickname: str, domain: str, http_prefix: str, post_filename: str, - recent_posts_cache: {}, debug: bool) -> None: + recent_posts_cache: {}, debug: bool, + manual: bool) -> None: """Removes replies when deleting a post """ replies_filename = post_filename.replace('.json', '.replies') @@ -1626,7 +1636,7 @@ def _delete_post_remove_replies(base_dir: str, nickname: str, domain: str, if os.path.isfile(reply_file): delete_post(base_dir, http_prefix, nickname, domain, reply_file, debug, - recent_posts_cache) + recent_posts_cache, manual) # remove the replies file try: os.remove(replies_filename) @@ -1794,9 +1804,53 @@ def _delete_conversation_post(base_dir: str, nickname: str, domain: str, str(conversation_filename)) +def is_dm(post_json_object: {}) -> bool: + """Returns true if the given post is a DM + """ + if post_json_object['type'] != 'Create': + return False + if not has_object_dict(post_json_object): + return False + if post_json_object['object']['type'] != 'ChatMessage': + if post_json_object['object']['type'] != 'Note' and \ + post_json_object['object']['type'] != 'Page' and \ + post_json_object['object']['type'] != 'Patch' and \ + post_json_object['object']['type'] != 'EncryptedMessage' and \ + post_json_object['object']['type'] != 'Article': + return False + if post_json_object['object'].get('moderationStatus'): + return False + fields = ('to', 'cc') + for field_name in fields: + if not post_json_object['object'].get(field_name): + continue + for to_address in post_json_object['object'][field_name]: + if to_address.endswith('#Public'): + return False + if to_address.endswith('followers'): + return False + return True + + +def _is_remote_dm(domain_full: str, post_json_object: {}) -> bool: + """Is the given post a DM from a different domain? + """ + if not is_dm(post_json_object): + return False + this_post_json = post_json_object + if has_object_dict(post_json_object): + this_post_json = post_json_object['object'] + if this_post_json.get('attributedTo'): + if isinstance(this_post_json['attributedTo'], str): + if '://' + domain_full not in this_post_json['attributedTo']: + return True + return False + + def delete_post(base_dir: str, http_prefix: str, nickname: str, domain: str, post_filename: str, - debug: bool, recent_posts_cache: {}) -> None: + debug: bool, recent_posts_cache: {}, + manual: bool) -> None: """Recursively deletes a post and its replies and attachments """ post_json_object = load_json(post_filename, 1) @@ -1804,7 +1858,7 @@ def delete_post(base_dir: str, http_prefix: str, # remove any replies _delete_post_remove_replies(base_dir, nickname, domain, http_prefix, post_filename, - recent_posts_cache, debug) + recent_posts_cache, debug, manual) # finally, remove the post itself try: os.remove(post_filename) @@ -1814,6 +1868,13 @@ def delete_post(base_dir: str, http_prefix: str, str(post_filename)) return + # don't allow DMs to be deleted if they came from a different instance + # otherwise this breaks expectations about how DMs should operate + # i.e. DMs should only be removed if they are manually deleted + if not manual: + if _is_remote_dm(domain, post_json_object): + return + # don't allow deletion of bookmarked posts if _is_bookmarked(base_dir, nickname, domain, post_filename): return @@ -1874,7 +1935,7 @@ def delete_post(base_dir: str, http_prefix: str, # remove any replies _delete_post_remove_replies(base_dir, nickname, domain, http_prefix, post_filename, - recent_posts_cache, debug) + recent_posts_cache, debug, manual) # finally, remove the post itself try: os.remove(post_filename) @@ -2769,34 +2830,6 @@ def is_chat_message(post_json_object: {}) -> bool: return True -def is_dm(post_json_object: {}) -> bool: - """Returns true if the given post is a DM - """ - if post_json_object['type'] != 'Create': - return False - if not has_object_dict(post_json_object): - return False - if post_json_object['object']['type'] != 'ChatMessage': - if post_json_object['object']['type'] != 'Note' and \ - post_json_object['object']['type'] != 'Page' and \ - post_json_object['object']['type'] != 'Patch' and \ - post_json_object['object']['type'] != 'EncryptedMessage' and \ - post_json_object['object']['type'] != 'Article': - return False - if post_json_object['object'].get('moderationStatus'): - return False - fields = ('to', 'cc') - for field_name in fields: - if not post_json_object['object'].get(field_name): - continue - for to_address in post_json_object['object'][field_name]: - if to_address.endswith('#Public'): - return False - if to_address.endswith('followers'): - return False - return True - - def is_reply(post_json_object: {}, actor: str) -> bool: """Returns true if the given post is a reply to the given actor """ @@ -3037,15 +3070,6 @@ def user_agent_domain(user_agent: str, debug: bool) -> str: return agent_domain -def has_object_dict(post_json_object: {}) -> bool: - """Returns true if the given post has an object dict - """ - if post_json_object.get('object'): - if isinstance(post_json_object['object'], dict): - return True - return False - - def get_alt_path(actor: str, domain_full: str, calling_domain: str) -> str: """Returns alternate path from the actor eg. https://clearnetdomain/path becomes http://oniondomain/path