diff --git a/daemon.py b/daemon.py index b9b710bef..c4ebce580 100644 --- a/daemon.py +++ b/daemon.py @@ -415,6 +415,8 @@ from crawlers import load_known_web_bots from qrcode import save_domain_qrcode from importFollowing import run_import_following_watchdog from maps import map_format_from_tagmaps_path +from relationships import get_moved_feed +from relationships import update_moved_actors import os @@ -3005,6 +3007,11 @@ class PubServer(BaseHTTPRequestHandler): if '&' in options_actor: options_actor = options_actor.split('&')[0] + # actor for the movedTo + options_actor_moved = options_confirm_params.split('movedToActor=')[1] + if '&' in options_actor_moved: + options_actor_moved = options_actor_moved.split('&')[0] + # url of the avatar options_avatar_url = options_confirm_params.split('avatarUrl=')[1] if '&' in options_avatar_url: @@ -3412,6 +3419,25 @@ class PubServer(BaseHTTPRequestHandler): self.server.postreq_busy = False return + # person options screen, move button + # See html_person_options followStr + if '&submitMove=' in options_confirm_params: + if debug: + print('Moving ' + options_actor_moved) + msg = \ + html_confirm_follow(self.server.translate, + base_dir, + users_path, + options_actor_moved, + options_avatar_url).encode('utf-8') + if msg: + msglen = len(msg) + self._set_headers('text/html', msglen, + cookie, calling_domain, False) + self._write(msg) + self.server.postreq_busy = False + return + # person options screen, unfollow button # See html_person_options followStr if '&submitUnfollow=' in options_confirm_params or \ @@ -8383,7 +8409,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.news_instance, authorized, access_keys, is_group, - self.server.theme_name) + self.server.theme_name, + self.server.blocked_cache) if msg: msg = msg.encode('utf-8') msglen = len(msg) @@ -14363,6 +14390,142 @@ class PubServer(BaseHTTPRequestHandler): return True return False + def _show_moved_feed(self, authorized: bool, + calling_domain: str, referer_domain: str, + path: str, base_dir: str, http_prefix: str, + domain: str, port: int, getreq_start_time, + proxy_type: str, cookie: str, + debug: str, curr_session) -> bool: + """Shows the moved feed + """ + following = \ + get_moved_feed(base_dir, domain, port, path, + http_prefix, authorized, FOLLOWS_PER_PAGE) + if following: + if self._request_http(): + page_number = 1 + if '?page=' not in path: + search_path = path + # get a page of following, not the summary + following = \ + get_moved_feed(base_dir, domain, port, path, + http_prefix, authorized, + FOLLOWS_PER_PAGE) + else: + page_number_str = path.split('?page=')[1] + if ';' in page_number_str: + page_number_str = page_number_str.split(';')[0] + if '#' in page_number_str: + page_number_str = page_number_str.split('#')[0] + if len(page_number_str) > 5: + page_number_str = "1" + if page_number_str.isdigit(): + page_number = int(page_number_str) + search_path = path.split('?page=')[0] + get_person = \ + person_lookup(domain, + search_path.replace('/moved', ''), + base_dir) + if get_person: + curr_session = \ + self._establish_session("show_moved_feed", + curr_session, proxy_type) + if not curr_session: + self._404() + return True + + access_keys = self.server.access_keys + city = None + timezone = None + if '/users/' in path: + nickname = path.split('/users/')[1] + if '/' in nickname: + nickname = nickname.split('/')[0] + if self.server.key_shortcuts.get(nickname): + access_keys = \ + self.server.key_shortcuts[nickname] + + city = get_spoofed_city(self.server.city, + base_dir, nickname, domain) + if self.server.account_timezone.get(nickname): + timezone = \ + self.server.account_timezone.get(nickname) + content_license_url = \ + self.server.content_license_url + shared_items_federated_domains = \ + self.server.shared_items_federated_domains + bold_reading = False + if self.server.bold_reading.get(nickname): + bold_reading = True + msg = \ + html_profile(self.server.signing_priv_key_pem, + self.server.rss_icon_at_top, + self.server.icons_as_buttons, + self.server.default_timeline, + self.server.recent_posts_cache, + self.server.max_recent_posts, + self.server.translate, + self.server.project_version, + base_dir, http_prefix, + authorized, + get_person, 'moved', + curr_session, + self.server.cached_webfingers, + self.server.person_cache, + self.server.yt_replace_domain, + self.server.twitter_replacement_domain, + self.server.show_published_date_only, + self.server.newswire, + self.server.theme_name, + self.server.dormant_months, + self.server.peertube_instances, + self.server.allow_local_network_access, + self.server.text_mode_banner, + self.server.debug, + access_keys, city, + self.server.system_language, + self.server.max_like_count, + shared_items_federated_domains, + following, + page_number, + FOLLOWS_PER_PAGE, + self.server.cw_lists, + self.server.lists_enabled, + content_license_url, + timezone, bold_reading).encode('utf-8') + msglen = len(msg) + self._set_headers('text/html', + msglen, cookie, calling_domain, False) + self._write(msg) + fitness_performance(getreq_start_time, + self.server.fitness, + '_GET', '_show_moved_feed', + debug) + return True + else: + if self._secure_mode(curr_session, proxy_type): + msg_str = json.dumps(following, + ensure_ascii=False) + msg_str = self._convert_domains(calling_domain, + referer_domain, + msg_str) + msg = msg_str.encode('utf-8') + msglen = len(msg) + accept_str = self.headers['Accept'] + protocol_str = \ + get_json_content_from_accept(accept_str) + self._set_headers(protocol_str, msglen, + None, calling_domain, False) + self._write(msg) + fitness_performance(getreq_start_time, + self.server.fitness, + '_GET', '_show_moved_feed json', + debug) + else: + self._404() + return True + return False + def _show_followers_feed(self, authorized: bool, calling_domain: str, referer_domain: str, path: str, base_dir: str, http_prefix: str, @@ -19147,6 +19310,24 @@ class PubServer(BaseHTTPRequestHandler): '_GET', 'show profile 3 done', self.server.debug) + if self._show_moved_feed(authorized, + calling_domain, referer_domain, + self.path, + self.server.base_dir, + self.server.http_prefix, + self.server.domain, + self.server.port, + getreq_start_time, + proxy_type, + cookie, self.server.debug, + curr_session): + self.server.getreq_busy = False + return + + fitness_performance(getreq_start_time, self.server.fitness, + '_GET', 'show moved 4 done', + self.server.debug) + if self._show_followers_feed(authorized, calling_domain, referer_domain, self.path, @@ -19162,7 +19343,7 @@ class PubServer(BaseHTTPRequestHandler): return fitness_performance(getreq_start_time, self.server.fitness, - '_GET', 'show profile 4 done', + '_GET', 'show profile 5 done', self.server.debug) # look up a person @@ -21922,6 +22103,8 @@ def run_daemon(max_hashtags: int, print('Invalid domain: ' + domain) return + update_moved_actors(base_dir, debug) + if unit_test: server_address = (domain, proxy_port) pub_handler = partial(PubServerUnitTest) @@ -22018,6 +22201,7 @@ def run_daemon(max_hashtags: int, 'Page down': '.', 'submitButton': 'y', 'followButton': 'f', + 'moveButton': 'm', 'blockButton': 'b', 'infoButton': 'i', 'snoozeButton': 's', diff --git a/epicyon.py b/epicyon.py index 6f078742d..aea29be4f 100644 --- a/epicyon.py +++ b/epicyon.py @@ -111,6 +111,7 @@ from desktop_client import run_desktop_client from happening import dav_month_via_server from happening import dav_day_via_server from content import import_emoji +from relationships import get_moved_accounts def str2bool(value_str) -> bool: @@ -343,6 +344,9 @@ def _command_options() -> None: parser.add_argument('--posts', dest='posts', type=str, default=None, help='Show posts for the given handle') + parser.add_argument('--moved', dest='moved', type=str, + default=None, + help='Show moved accounts for the given handle') parser.add_argument('--postDomains', dest='postDomains', type=str, default=None, help='Show domains referenced in public ' @@ -860,6 +864,45 @@ def _command_options() -> None: signing_priv_key_pem, origin_domain) sys.exit() + if argb.moved: + if not argb.domain: + origin_domain = get_config_param(base_dir, 'domain') + else: + origin_domain = argb.domain + if debug: + print('origin_domain: ' + str(origin_domain)) + if '@' not in argb.moved: + if '/users/' in argb.moved: + moved_nickname = get_nickname_from_actor(argb.moved) + moved_domain, moved_port = get_domain_from_actor(argb.moved) + argb.moved = \ + get_full_domain(moved_nickname + '@' + moved_domain, + moved_port) + else: + print('Syntax: --moved nickname@domain') + sys.exit() + if not argb.http: + argb.port = 443 + nickname = argb.moved.split('@')[0] + domain = argb.moved.split('@')[1] + proxy_type = None + if argb.tor or domain.endswith('.onion'): + proxy_type = 'tor' + if domain.endswith('.onion'): + argb.port = 80 + elif argb.i2p or domain.endswith('.i2p'): + proxy_type = 'i2p' + if domain.endswith('.i2p'): + argb.port = 80 + elif argb.gnunet: + proxy_type = 'gnunet' + if not argb.language: + argb.language = 'en' + moved_dict = \ + get_moved_accounts(base_dir, nickname, domain, 'following.txt') + pprint(moved_dict) + sys.exit() + if argb.postDomains: if '@' not in argb.postDomains: if '/users/' in argb.postDomains: diff --git a/inbox.py b/inbox.py index 0c198bc70..2789ee364 100644 --- a/inbox.py +++ b/inbox.py @@ -1088,6 +1088,39 @@ def _person_receive_update(base_dir: str, if debug: print('actor updated for ' + person_json['id']) + if person_json.get('movedTo'): + prev_domain, prev_port = get_domain_from_actor(person_json['id']) + prev_domain_full = get_full_domain(prev_domain, prev_port) + prev_nickname = get_nickname_from_actor(person_json['id']) + new_domain, new_port = get_domain_from_actor(person_json['movedTo']) + new_domain_full = get_full_domain(new_domain, new_port) + new_nickname = get_nickname_from_actor(person_json['movedTo']) + + new_actor = prev_nickname + '@' + prev_domain_full + ' ' + \ + new_nickname + '@' + new_domain_full + refollow_str = '' + refollow_filename = base_dir + '/accounts/actors_moved.txt' + refollow_file_exists = False + if os.path.isfile(refollow_filename): + try: + with open(refollow_filename, 'r', + encoding='utf-8') as fp_refollow: + refollow_str = fp_refollow.read() + refollow_file_exists = True + except OSError: + print('EX: unable to read ' + refollow_filename) + if new_actor not in refollow_str: + refollow_type = 'w+' + if refollow_file_exists: + refollow_type = 'a+' + try: + with open(refollow_filename, refollow_type, + encoding='utf-8') as fp_refollow: + fp_refollow.write(new_actor + '\n') + except OSError: + print('EX: unable to write to ' + + refollow_filename) + # remove avatar if it exists so that it will be refreshed later # when a timeline is constructed actor_str = person_json['id'].replace('/', '-') diff --git a/migrate.py b/migrate.py index d2252c45b..ca5e7853d 100644 --- a/migrate.py +++ b/migrate.py @@ -129,59 +129,61 @@ def _update_moved_handle(base_dir: str, nickname: str, domain: str, following_filename = \ acct_dir(base_dir, nickname, domain) + '/following.txt' if os.path.isfile(following_filename): + following_handles = [] with open(following_filename, 'r', encoding='utf-8') as foll1: following_handles = foll1.readlines() - moved_to_handle = moved_to_nickname + '@' + moved_to_domain_full - handle_lower = handle.lower() + moved_to_handle = moved_to_nickname + '@' + moved_to_domain_full + handle_lower = handle.lower() - refollow_filename = \ - acct_dir(base_dir, nickname, domain) + '/refollow.txt' + refollow_filename = \ + acct_dir(base_dir, nickname, domain) + '/refollow.txt' - # unfollow the old handle - with open(following_filename, 'w+', encoding='utf-8') as foll2: - for follow_handle in following_handles: - if follow_handle.strip("\n").strip("\r").lower() != \ - handle_lower: - foll2.write(follow_handle) + # unfollow the old handle + with open(following_filename, 'w+', encoding='utf-8') as foll2: + for follow_handle in following_handles: + if follow_handle.strip("\n").strip("\r").lower() != \ + handle_lower: + foll2.write(follow_handle) + else: + handle_nickname = handle.split('@')[0] + handle_domain = handle.split('@')[1] + unfollow_account(base_dir, nickname, domain, + handle_nickname, + handle_domain, + debug, group_account, 'following.txt') + ctr += 1 + print('Unfollowed ' + handle + ' who has moved to ' + + moved_to_handle) + + # save the new handles to the refollow list + if os.path.isfile(refollow_filename): + with open(refollow_filename, 'a+', + encoding='utf-8') as refoll: + refoll.write(moved_to_handle + '\n') else: - handle_nickname = handle.split('@')[0] - handle_domain = handle.split('@')[1] - unfollow_account(base_dir, nickname, domain, - handle_nickname, - handle_domain, - debug, group_account, 'following.txt') - ctr += 1 - print('Unfollowed ' + handle + ' who has moved to ' + - moved_to_handle) - - # save the new handles to the refollow list - if os.path.isfile(refollow_filename): - with open(refollow_filename, 'a+', - encoding='utf-8') as refoll: - refoll.write(moved_to_handle + '\n') - else: - with open(refollow_filename, 'w+', - encoding='utf-8') as refoll: - refoll.write(moved_to_handle + '\n') + with open(refollow_filename, 'w+', + encoding='utf-8') as refoll: + refoll.write(moved_to_handle + '\n') followers_filename = \ acct_dir(base_dir, nickname, domain) + '/followers.txt' if os.path.isfile(followers_filename): + follower_handles = [] with open(followers_filename, 'r', encoding='utf-8') as foll3: follower_handles = foll3.readlines() - handle_lower = handle.lower() + handle_lower = handle.lower() - # remove followers who have moved - with open(followers_filename, 'w+', encoding='utf-8') as foll4: - for follower_handle in follower_handles: - if follower_handle.strip("\n").strip("\r").lower() != \ - handle_lower: - foll4.write(follower_handle) - else: - ctr += 1 - print('Removed follower who has moved ' + handle) + # remove followers who have moved + with open(followers_filename, 'w+', encoding='utf-8') as foll4: + for follower_handle in follower_handles: + if follower_handle.strip("\n").strip("\r").lower() != \ + handle_lower: + foll4.write(follower_handle) + else: + ctr += 1 + print('Removed follower who has moved ' + handle) return ctr diff --git a/relationships.py b/relationships.py new file mode 100644 index 000000000..3e86b26c2 --- /dev/null +++ b/relationships.py @@ -0,0 +1,292 @@ +__filename__ = "relationships.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.3.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@libreserver.org" +__status__ = "Production" +__module_group__ = "Core" + +import os +from utils import acct_dir +from utils import valid_nickname +from utils import get_full_domain +from utils import local_actor_url +from utils import remove_domain_port +from utils import remove_eol +from utils import is_account_dir +from utils import get_nickname_from_actor +from utils import get_domain_from_actor +from utils import load_json + + +def get_moved_accounts(base_dir: str, nickname: str, domain: str, + filename: str = 'following.txt') -> {}: + """returns a dict of moved accounts + """ + moved_accounts_filename = base_dir + '/accounts/actors_moved.txt' + if not os.path.isfile(moved_accounts_filename): + return {} + refollow_str = '' + try: + with open(moved_accounts_filename, 'r', + encoding='utf-8') as fp_refollow: + refollow_str = fp_refollow.read() + except OSError: + print('EX: get_moved_accounts unable to read ' + + moved_accounts_filename) + refollow_list = refollow_str.split('\n') + refollow_dict = {} + + follow_filename = \ + acct_dir(base_dir, nickname, domain) + '/' + filename + follow_str = '' + try: + with open(follow_filename, 'r', + encoding='utf-8') as fp_follow: + follow_str = fp_follow.read() + except OSError: + print('EX: get_moved_accounts unable to read ' + + follow_filename) + follow_list = follow_str.split('\n') + + ctr = 0 + for line in refollow_list: + if ' ' not in line: + continue + prev_handle = line.split(' ')[0] + new_handle = line.split(' ')[1] + refollow_dict[prev_handle] = new_handle + ctr = ctr + 1 + + result = {} + for handle in follow_list: + if refollow_dict.get(handle): + if refollow_dict[handle] not in follow_list: + result[handle] = refollow_dict[handle] + return result + + +def get_moved_feed(base_dir: str, domain: str, port: int, path: str, + http_prefix: str, authorized: bool, + follows_per_page=12) -> {}: + """Returns the moved accounts feed from GET requests. + """ + # Don't show moved accounts to non-authorized viewers + if not authorized: + follows_per_page = 0 + + if '/moved' not in path: + return None + if '?page=' not in path: + path = path.replace('/moved', '/moved?page=true') + # handle page numbers + header_only = True + page_number = None + if '?page=' in path: + page_number = path.split('?page=')[1] + if len(page_number) > 5: + page_number = "1" + if page_number == 'true' or not authorized: + page_number = 1 + else: + try: + page_number = int(page_number) + except BaseException: + print('EX: get_moved_feed unable to convert to int ' + + str(page_number)) + path = path.split('?page=')[0] + header_only = False + + if not path.endswith('/moved'): + return None + nickname = None + if path.startswith('/users/'): + nickname = \ + path.replace('/users/', '', 1).replace('/moved', '') + if path.startswith('/@'): + nickname = path.replace('/@', '', 1).replace('/moved', '') + if not nickname: + return None + if not valid_nickname(domain, nickname): + return None + + domain = get_full_domain(domain, port) + + lines = get_moved_accounts(base_dir, nickname, domain, + 'following.txt') + + if header_only: + first_str = \ + local_actor_url(http_prefix, nickname, domain) + \ + '/moved?page=1' + id_str = \ + local_actor_url(http_prefix, nickname, domain) + '/moved' + total_str = str(len(lines.items())) + following = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'first': first_str, + 'id': id_str, + 'orderedItems': [], + 'totalItems': total_str, + 'type': 'OrderedCollection' + } + return following + + if not page_number: + page_number = 1 + + next_page_number = int(page_number + 1) + id_str = \ + local_actor_url(http_prefix, nickname, domain) + \ + '/moved?page=' + str(page_number) + part_of_str = \ + local_actor_url(http_prefix, nickname, domain) + '/moved' + following = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': id_str, + 'orderedItems': [], + 'partOf': part_of_str, + 'totalItems': 0, + 'type': 'OrderedCollectionPage' + } + + handle_domain = domain + handle_domain = remove_domain_port(handle_domain) + curr_page = 1 + page_ctr = 0 + total_ctr = 0 + for handle, new_handle in lines.items(): + # nickname@domain + page_ctr += 1 + total_ctr += 1 + if curr_page == page_number: + line2_lower = handle.lower() + line2 = remove_eol(line2_lower) + nick = line2.split('@')[0] + dom = line2.split('@')[1] + if not nick.startswith('!'): + # person actor + url = local_actor_url(http_prefix, nick, dom) + else: + # group actor + url = http_prefix + '://' + dom + '/c/' + nick + following['orderedItems'].append(url) + if page_ctr >= follows_per_page: + page_ctr = 0 + curr_page += 1 + following['totalItems'] = total_ctr + last_page = int(total_ctr / follows_per_page) + last_page = max(last_page, 1) + if next_page_number > last_page: + following['next'] = \ + local_actor_url(http_prefix, nickname, domain) + \ + '/moved?page=' + str(last_page) + return following + + +def update_moved_actors(base_dir: str, debug: bool) -> None: + """Updates the file containing moved actors + """ + actors_cache_dir = base_dir + '/cache/actors' + if not os.path.isdir(actors_cache_dir): + if debug: + print('No cached actors') + return + + if debug: + print('Updating moved actors') + actors_dict = {} + ctr = 0 + for _, _, files in os.walk(actors_cache_dir): + for actor_str in files: + if not actor_str.endswith('.json'): + continue + orig_str = actor_str + actor_str = actor_str.replace('.json', '').replace('#', '/') + nickname = get_nickname_from_actor(actor_str) + domain, port = get_domain_from_actor(actor_str) + domain_full = get_full_domain(domain, port) + handle = nickname + '@' + domain_full + actors_dict[handle] = orig_str + ctr += 1 + break + + if actors_dict: + print('Actors dict created ' + str(ctr)) + else: + print('No cached actors found') + + # get the handles to be checked for movedTo attribute + handles_to_check = [] + for _, dirs, _ in os.walk(base_dir + '/accounts'): + for account in dirs: + if not is_account_dir(account): + continue + following_filename = \ + base_dir + '/accounts/' + account + '/following.txt' + if not os.path.isfile(following_filename): + continue + following_str = '' + try: + with open(following_filename, 'r', + encoding='utf-8') as fp_foll: + following_str = fp_foll.read() + except OSError: + print('EX: update_moved_actors unable to read ' + + following_filename) + continue + following_list = following_str.split('\n') + for handle in following_list: + if handle not in handles_to_check: + handles_to_check.append(handle) + break + + if handles_to_check: + print('All accounts handles list generated ' + + str(len(handles_to_check))) + else: + print('No accounts are following') + + moved_str = '' + ctr = 0 + for handle in handles_to_check: + if not actors_dict.get(handle): + continue + actor_filename = base_dir + '/cache/actors/' + actors_dict[handle] + if not os.path.isfile(actor_filename): + continue + actor_json = load_json(actor_filename, 1, 1) + if not actor_json: + continue + if not actor_json.get('movedTo'): + continue + nickname = get_nickname_from_actor(actor_json['movedTo']) + domain, port = get_domain_from_actor(actor_json['movedTo']) + domain_full = get_full_domain(domain, port) + new_handle = nickname + '@' + domain_full + moved_str += handle + ' ' + new_handle + '\n' + ctr = ctr + 1 + + if moved_str: + print('Moved accounts detected ' + str(ctr)) + else: + print('No moved accounts detected') + + moved_accounts_filename = base_dir + '/accounts/actors_moved.txt' + if not moved_str: + if os.path.isfile(moved_accounts_filename): + try: + os.remove(moved_accounts_filename) + except OSError: + print('EX: update_moved_actors unable to remove ' + + moved_accounts_filename) + return + + try: + with open(moved_accounts_filename, 'w+', + encoding='utf-8') as fp_moved: + fp_moved.write(moved_str) + except OSError: + print('EX: update_moved_actors unable to save ' + + moved_accounts_filename) diff --git a/translations/ar.json b/translations/ar.json index fa4713dbf..aebe2413b 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -602,5 +602,7 @@ "Minimize all images": "تصغير كل الصور", "Edit post": "تعديل المنشور", "Preview posts on profile screen": "معاينة المشاركات على شاشة الملف الشخصي", - "Reverse timelines": "عكس الجداول الزمنية" + "Reverse timelines": "عكس الجداول الزمنية", + "Moved": "انتقل", + "Move": "يتحرك" } diff --git a/translations/bn.json b/translations/bn.json index 7d29655b1..7354466bd 100644 --- a/translations/bn.json +++ b/translations/bn.json @@ -602,5 +602,7 @@ "Minimize all images": "সমস্ত ছবি ছোট করুন", "Edit post": "পোস্ট সম্পাদনা করুন", "Preview posts on profile screen": "প্রোফাইল স্ক্রিনে পোস্টের পূর্বরূপ দেখুন", - "Reverse timelines": "বিপরীত সময়রেখা" + "Reverse timelines": "বিপরীত সময়রেখা", + "Moved": "সরানো হয়েছে", + "Move": "সরান" } diff --git a/translations/ca.json b/translations/ca.json index d67b78592..a93d01f78 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -602,5 +602,7 @@ "Minimize all images": "Minimitzar totes les imatges", "Edit post": "Edita la publicació", "Preview posts on profile screen": "Previsualitza les publicacions a la pantalla del perfil", - "Reverse timelines": "Cronologia inversa" + "Reverse timelines": "Cronologia inversa", + "Moved": "Mogut", + "Move": "Moure's" } diff --git a/translations/cy.json b/translations/cy.json index c12103107..127cc8b11 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -602,5 +602,7 @@ "Minimize all images": "Lleihau pob delwedd", "Edit post": "Golygu post", "Preview posts on profile screen": "Rhagolwg postiadau ar sgrin proffil", - "Reverse timelines": "Gwrthdroi llinellau amser" + "Reverse timelines": "Gwrthdroi llinellau amser", + "Moved": "Wedi symud", + "Move": "Symud" } diff --git a/translations/de.json b/translations/de.json index 72c4c5bf3..dfdba1016 100644 --- a/translations/de.json +++ b/translations/de.json @@ -602,5 +602,7 @@ "Minimize all images": "Alle Bilder minimieren", "Edit post": "Beitrag bearbeiten", "Preview posts on profile screen": "Vorschau von Beiträgen auf dem Profilbildschirm", - "Reverse timelines": "Umgekehrte Zeitlinien" + "Reverse timelines": "Umgekehrte Zeitlinien", + "Moved": "Gerührt", + "Move": "Bewegen" } diff --git a/translations/el.json b/translations/el.json index cf04ff8d0..ed3a0a7af 100644 --- a/translations/el.json +++ b/translations/el.json @@ -602,5 +602,7 @@ "Minimize all images": "Ελαχιστοποίηση όλων των εικόνων", "Edit post": "Επεξεργασία ανάρτησης", "Preview posts on profile screen": "Προεπισκόπηση αναρτήσεων στην οθόνη προφίλ", - "Reverse timelines": "Αντίστροφα χρονοδιαγράμματα" + "Reverse timelines": "Αντίστροφα χρονοδιαγράμματα", + "Moved": "Μετακινήθηκε", + "Move": "Κίνηση" } diff --git a/translations/en.json b/translations/en.json index 9799205e6..a3a3bf8b3 100644 --- a/translations/en.json +++ b/translations/en.json @@ -602,5 +602,7 @@ "Minimize all images": "Minimize all images", "Edit post": "Edit post", "Preview posts on profile screen": "Preview posts on profile screen", - "Reverse timelines": "Reverse timelines" + "Reverse timelines": "Reverse timelines", + "Moved": "Moved", + "Move": "Move" } diff --git a/translations/es.json b/translations/es.json index 5e8192763..c7b75bcf2 100644 --- a/translations/es.json +++ b/translations/es.json @@ -602,5 +602,7 @@ "Minimize all images": "Minimizar todas las imágenes", "Edit post": "Editar post", "Preview posts on profile screen": "Vista previa de publicaciones en la pantalla de perfil", - "Reverse timelines": "Líneas de tiempo inversas" + "Reverse timelines": "Líneas de tiempo inversas", + "Moved": "Movida", + "Move": "Muevete" } diff --git a/translations/fa.json b/translations/fa.json index e7163e242..469a984b6 100644 --- a/translations/fa.json +++ b/translations/fa.json @@ -602,5 +602,7 @@ "Minimize all images": "تمام تصاویر را به حداقل برسانید", "Edit post": "ویرایش پست", "Preview posts on profile screen": "پیش نمایش پست ها در صفحه نمایه", - "Reverse timelines": "جدول های زمانی معکوس" + "Reverse timelines": "جدول های زمانی معکوس", + "Moved": "منتقل شد", + "Move": "حرکت" } diff --git a/translations/fr.json b/translations/fr.json index fb339e044..a17d18394 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -602,5 +602,7 @@ "Minimize all images": "Réduire toutes les images", "Edit post": "Modifier le message", "Preview posts on profile screen": "Prévisualiser les messages sur l'écran de profil", - "Reverse timelines": "Chronologies inversées" + "Reverse timelines": "Chronologies inversées", + "Moved": "Déplacée", + "Move": "Déplacer" } diff --git a/translations/ga.json b/translations/ga.json index a79625165..19e0b43f6 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -602,5 +602,7 @@ "Minimize all images": "Íoslaghdaigh gach íomhá", "Edit post": "Cuir postáil in eagar", "Preview posts on profile screen": "Réamhamhairc postálacha ar an scáileán próifíle", - "Reverse timelines": "Amlínte droim ar ais" + "Reverse timelines": "Amlínte droim ar ais", + "Moved": "Ar athraíodh a ionad", + "Move": "Bog" } diff --git a/translations/hi.json b/translations/hi.json index deb79d897..ba02685dd 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -602,5 +602,7 @@ "Minimize all images": "सभी छवियों को छोटा करें", "Edit post": "संपादित पोस्ट", "Preview posts on profile screen": "प्रोफ़ाइल स्क्रीन पर पोस्ट का पूर्वावलोकन करें", - "Reverse timelines": "रिवर्स टाइमलाइन" + "Reverse timelines": "रिवर्स टाइमलाइन", + "Moved": "ले जाया गया", + "Move": "कदम" } diff --git a/translations/it.json b/translations/it.json index c4ec40463..f7f8cda26 100644 --- a/translations/it.json +++ b/translations/it.json @@ -602,5 +602,7 @@ "Minimize all images": "Riduci a icona tutte le immagini", "Edit post": "Modifica post", "Preview posts on profile screen": "Visualizza l'anteprima dei post nella schermata del profilo", - "Reverse timelines": "Invertire le tempistiche" + "Reverse timelines": "Invertire le tempistiche", + "Moved": "Mosso", + "Move": "Spostare" } diff --git a/translations/ja.json b/translations/ja.json index 68b15e773..ea6e57eea 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -602,5 +602,7 @@ "Minimize all images": "すべての画像を最小化", "Edit post": "投稿を編集", "Preview posts on profile screen": "プロフィール画面で投稿をプレビュー", - "Reverse timelines": "逆タイムライン" + "Reverse timelines": "逆タイムライン", + "Moved": "移動しました", + "Move": "動く" } diff --git a/translations/ko.json b/translations/ko.json index 09722272a..25f478338 100644 --- a/translations/ko.json +++ b/translations/ko.json @@ -602,5 +602,7 @@ "Minimize all images": "모든 이미지 최소화", "Edit post": "게시물 수정", "Preview posts on profile screen": "프로필 화면에서 게시물 미리보기", - "Reverse timelines": "역방향 타임라인" + "Reverse timelines": "역방향 타임라인", + "Moved": "움직이는", + "Move": "이동하다" } diff --git a/translations/ku.json b/translations/ku.json index 8e7b5ee3b..5b54daa6b 100644 --- a/translations/ku.json +++ b/translations/ku.json @@ -602,5 +602,7 @@ "Minimize all images": "Hemî wêneyan kêm bikin", "Edit post": "Biguherîne post", "Preview posts on profile screen": "Mesajên li ser ekrana profîlê pêşdîtin", - "Reverse timelines": "Reverse timelines" + "Reverse timelines": "Reverse timelines", + "Moved": "Moved", + "Move": "Barkirin" } diff --git a/translations/nl.json b/translations/nl.json index 933f1ecf3..b37327e50 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -602,5 +602,7 @@ "Minimize all images": "Alle afbeeldingen minimaliseren", "Edit post": "Bericht bewerken", "Preview posts on profile screen": "Bekijk berichten op het profielscherm", - "Reverse timelines": "Omgekeerde tijdlijnen" + "Reverse timelines": "Omgekeerde tijdlijnen", + "Moved": "Verhuisd", + "Move": "Beweging" } diff --git a/translations/oc.json b/translations/oc.json index 3fb7854f8..6d501052f 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -598,5 +598,7 @@ "Minimize all images": "Minimize all images", "Edit post": "Edit post", "Preview posts on profile screen": "Preview posts on profile screen", - "Reverse timelines": "Reverse timelines" + "Reverse timelines": "Reverse timelines", + "Moved": "Moved", + "Move": "Move" } diff --git a/translations/pl.json b/translations/pl.json index 8d2b72a20..44ecbbc80 100644 --- a/translations/pl.json +++ b/translations/pl.json @@ -602,5 +602,7 @@ "Minimize all images": "Zminimalizuj wszystkie obrazy", "Edit post": "Edytuj post", "Preview posts on profile screen": "Podgląd postów na ekranie profilu", - "Reverse timelines": "Odwróć ramy czasowe" + "Reverse timelines": "Odwróć ramy czasowe", + "Moved": "Przeniósł", + "Move": "Przenosić" } diff --git a/translations/pt.json b/translations/pt.json index 23c0b6dbd..a7ea17e39 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -602,5 +602,7 @@ "Minimize all images": "Minimize todas as imagens", "Edit post": "Editar post", "Preview posts on profile screen": "Visualizar postagens na tela do perfil", - "Reverse timelines": "Cronogramas reversos" + "Reverse timelines": "Cronogramas reversos", + "Moved": "Mudou-se", + "Move": "Jogada" } diff --git a/translations/ru.json b/translations/ru.json index 474607369..3536c1dbb 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -602,5 +602,7 @@ "Minimize all images": "Свернуть все изображения", "Edit post": "Редактировать сообщение", "Preview posts on profile screen": "Предварительный просмотр сообщений на экране профиля", - "Reverse timelines": "Обратные сроки" + "Reverse timelines": "Обратные сроки", + "Moved": "Взолнованный", + "Move": "Шаг" } diff --git a/translations/sw.json b/translations/sw.json index 6c2f0da49..1670892db 100644 --- a/translations/sw.json +++ b/translations/sw.json @@ -602,5 +602,7 @@ "Minimize all images": "Punguza picha zote", "Edit post": "Badilisha chapisho", "Preview posts on profile screen": "Hakiki machapisho kwenye skrini ya wasifu", - "Reverse timelines": "Обратные сроки" + "Reverse timelines": "Обратные сроки", + "Moved": "Imehamishwa", + "Move": "Sogeza" } diff --git a/translations/tr.json b/translations/tr.json index 0730e7864..63bc3d975 100644 --- a/translations/tr.json +++ b/translations/tr.json @@ -602,5 +602,7 @@ "Minimize all images": "Tüm görüntüleri simge durumuna küçült", "Edit post": "Gönderiyi düzenle", "Preview posts on profile screen": "Gönderileri profil ekranında önizleyin", - "Reverse timelines": "Обратные сроки" + "Reverse timelines": "Обратные сроки", + "Moved": "Etkilenmiş", + "Move": "Hareket" } diff --git a/translations/uk.json b/translations/uk.json index 2373f681c..361cec1c6 100644 --- a/translations/uk.json +++ b/translations/uk.json @@ -602,5 +602,7 @@ "Minimize all images": "Згорнути всі зображення", "Edit post": "Редагувати пост", "Preview posts on profile screen": "Попередній перегляд дописів на екрані профілю", - "Reverse timelines": "Обратные сроки" + "Reverse timelines": "Обратные сроки", + "Moved": "Переїхав", + "Move": "рухатися" } diff --git a/translations/yi.json b/translations/yi.json index 57c12a08e..72ef11def 100644 --- a/translations/yi.json +++ b/translations/yi.json @@ -602,5 +602,7 @@ "Minimize all images": "מינאַמייז אַלע בילדער", "Edit post": "רעדאַגירן פּאָסטן", "Preview posts on profile screen": "פאָרויסיקע ווייַזונג אַרטיקלען אויף פּראָפיל פאַרשטעלן", - "Reverse timelines": "פאַרקערט טיימליינז" + "Reverse timelines": "פאַרקערט טיימליינז", + "Moved": "אריבערגעפארן", + "Move": "מאַך" } diff --git a/translations/zh.json b/translations/zh.json index 92619799b..29c62ee29 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -602,5 +602,7 @@ "Minimize all images": "最小化所有图像", "Edit post": "编辑帖子", "Preview posts on profile screen": "在个人资料屏幕上预览帖子", - "Reverse timelines": "倒转时间线" + "Reverse timelines": "倒转时间线", + "Moved": "אריבערגעפארן", + "Move": "移动" } diff --git a/utils.py b/utils.py index 34ca8912e..496da8ea9 100644 --- a/utils.py +++ b/utils.py @@ -2240,7 +2240,7 @@ def _get_reserved_words() -> str: 'minimal', 'search', 'eventdelete', 'searchemoji', 'catalog', 'conversationId', 'mention', 'http', 'https', 'ipfs', 'ipns', - 'ontologies', 'data', 'postedit') + 'ontologies', 'data', 'postedit', 'moved') def get_nickname_validation_pattern() -> str: diff --git a/webapp_person_options.py b/webapp_person_options.py index 64f4a7b1e..e36b3d5e5 100644 --- a/webapp_person_options.py +++ b/webapp_person_options.py @@ -163,9 +163,11 @@ def html_person_options(default_timeline: str, authorized: bool, access_keys: {}, is_group: bool, - theme: str) -> str: + theme: str, + blocked_cache: []) -> str: """Show options for a person: view/follow/block/report """ + options_link_str = '' options_domain, options_port = get_domain_from_actor(options_actor) if not options_domain: return None @@ -211,9 +213,8 @@ def html_person_options(default_timeline: str, options_nickname, options_domain_full): block_str = 'Block' - options_link_str = '' if options_link: - options_link_str = \ + options_link_str += \ ' \n' css_filename = base_dir + '/epicyon-options.css' @@ -295,15 +296,32 @@ def html_person_options(default_timeline: str, if follows_you and authorized: options_str += \ '

' + translate['Follows you'] + '

\n' + options_str += '
\n' if moved_to: new_nickname = get_nickname_from_actor(moved_to) new_domain, _ = get_domain_from_actor(moved_to) if new_nickname and new_domain: new_handle = new_nickname + '@' + new_domain + blocked_icon_str = '' + if is_blocked(base_dir, nickname, domain, + new_nickname, new_domain, blocked_cache): + blocked_icon_str = '❌' options_str += \ '

' + \ translate['New account'] + \ - ': @' + new_handle + '

\n' + ': @' + new_handle + '' + \ + blocked_icon_str + if follow_str == 'Unfollow' and not blocked_icon_str: + options_str += \ + ' \n' + options_str += \ + '' + options_str += '

\n' elif also_known_as: other_accounts_html = \ '

' + \ @@ -332,7 +350,7 @@ def html_person_options(default_timeline: str, if email_address: options_str += \ - '

' + translate['Email'] + \ + '

' + translate['Email'] + \ ': ' + remove_html(email_address) + '

\n' if web_address: @@ -340,50 +358,51 @@ def html_person_options(default_timeline: str, if '://' not in web_str: web_str = 'https://' + web_str options_str += \ - '

🌐 ' + \ + '

🌐 ' + \ web_address + '

\n' if gemini_link: gemini_str = remove_html(gemini_link) if '://' not in gemini_str: gemini_str = 'gemini://' + gemini_str options_str += \ - '

' + \ + '

' + \ gemini_link + '

\n' if xmpp_address: options_str += \ - '

' + translate['XMPP'] + \ + '

' + translate['XMPP'] + \ ': ' + \ xmpp_address + '

\n' if matrix_address: options_str += \ - '

' + translate['Matrix'] + ': ' + \ + '

' + translate['Matrix'] + ': ' + \ remove_html(matrix_address) + '

\n' if ssb_address: options_str += \ - '

SSB: ' + remove_html(ssb_address) + '

\n' + '

SSB: ' + remove_html(ssb_address) + '

\n' if blog_address: options_str += \ - '

Blog: Blog: ' + \ remove_html(blog_address) + '

\n' if tox_address: options_str += \ - '

Tox: ' + remove_html(tox_address) + '

\n' + '

Tox: ' + remove_html(tox_address) + '

\n' if briar_address: if briar_address.startswith('briar://'): options_str += \ - '

' + \ + '

' + \ remove_html(briar_address) + '

\n' else: options_str += \ - '

briar://' + \ + '

briar://' + \ remove_html(briar_address) + '

\n' if cwtch_address: options_str += \ - '

Cwtch: ' + remove_html(cwtch_address) + '

\n' + '

Cwtch: ' + \ + remove_html(cwtch_address) + '

\n' if enigma_pub_key: options_str += \ - '

Enigma: ' + \ + '

Enigma: ' + \ remove_html(enigma_pub_key) + '

\n' if pgp_fingerprint: options_str += '

PGP: ' + \ @@ -391,8 +410,6 @@ def html_person_options(default_timeline: str, if pgp_pub_key: options_str += '

' + \ remove_html(pgp_pub_key).replace('\n', '
') + '

\n' - options_str += ' \n' options_str += ' \n' options_str += ' str: """Show the profile page as html """ + show_moved_accounts = False + if authorized: + moved_accounts_filename = base_dir + '/accounts/actors_moved.txt' + if os.path.isfile(moved_accounts_filename): + show_moved_accounts = True + nickname = profile_json['preferredUsername'] if not nickname: return "" @@ -679,6 +686,8 @@ def html_profile(signing_priv_key_pem: str, profile_description = standardize_text(profile_description) posts_button = 'button' following_button = 'button' + moved_button = 'button' + moved_button = 'button' followers_button = 'button' roles_button = 'button' skills_button = 'button' @@ -688,6 +697,8 @@ def html_profile(signing_priv_key_pem: str, posts_button = 'buttonselected' elif selected == 'following': following_button = 'buttonselected' + elif selected == 'moved': + moved_button = 'buttonselected' elif selected == 'followers': followers_button = 'buttonselected' elif selected == 'roles': @@ -979,14 +990,26 @@ def html_profile(signing_priv_key_pem: str, html_hide_from_screen_reader('✍') + ' ' + translate['Edit'] menu_followers = \ html_hide_from_screen_reader('👪') + ' ' + followers_str + if show_moved_accounts: + menu_moved = \ + html_hide_from_screen_reader('⌂') + ' ' + translate['Moved'] menu_logout = \ html_hide_from_screen_reader('❎') + ' ' + translate['Logout'] - nav_links = { - menu_timeline: user_path_str + '/' + deft, - menu_edit: user_path_str + '/editprofile', - menu_followers: user_path_str + '/followers#timeline', - menu_logout: '/logout' - } + if not show_moved_accounts: + nav_links = { + menu_timeline: user_path_str + '/' + deft, + menu_edit: user_path_str + '/editprofile', + menu_followers: user_path_str + '/followers#timeline', + menu_logout: '/logout' + } + else: + nav_links = { + menu_timeline: user_path_str + '/' + deft, + menu_edit: user_path_str + '/editprofile', + menu_followers: user_path_str + '/followers#timeline', + menu_moved: user_path_str + '/moved#timeline', + menu_logout: '/logout' + } if not is_group: menu_following = \ html_hide_from_screen_reader('👥') + ' ' + translate['Following'] @@ -1031,6 +1054,12 @@ def html_profile(signing_priv_key_pem: str, '' if not is_group: + if show_moved_accounts: + profile_str += \ + ' ' + \ + '' profile_str += \ ' ' + \ @@ -1108,6 +1137,19 @@ def html_profile(signing_priv_key_pem: str, max_items_per_page, dormant_months, debug, signing_priv_key_pem) + if show_moved_accounts and selected == 'moved': + profile_str += \ + _html_profile_following(translate, base_dir, http_prefix, + authorized, nickname, + domain, session, + cached_webfingers, + person_cache, extra_json, + project_version, ["moveAccount"], + selected, + users_path, page_number, + max_items_per_page, + dormant_months, debug, + signing_priv_key_pem) if selected == 'followers': profile_str += \ _html_profile_following(translate, base_dir, http_prefix, @@ -1199,9 +1241,10 @@ def _html_profile_posts(recent_posts_cache: {}, max_recent_posts: int, break shown_items = [] for item in outbox_feed['orderedItems']: - if not item.get('id'): - continue if item['type'] == 'Create': + if not item['object'].get('id'): + continue + item_id = remove_id_ending(item['object']['id']) post_str = \ individual_post_as_html(signing_priv_key_pem, True, recent_posts_cache, @@ -1227,9 +1270,9 @@ def _html_profile_posts(recent_posts_cache: {}, max_recent_posts: int, timezone, False, bold_reading, dogwhistles, minimize_all_images) - if post_str and item['id'] not in shown_items: + if post_str and item_id not in shown_items: profile_str += post_str + separator_str - shown_items.append(item['id']) + shown_items.append(item_id) ctr += 1 if ctr >= max_items: break @@ -2743,6 +2786,13 @@ def _individual_follow_as_html(signing_priv_key_pem: str, ';1;' + avatar_url + \ '">\n' + elif btn == 'moveAccount': + buttons_str += \ + '\n' result_str = '
\n' result_str += \