__filename__ = "webapp_profile.py" __author__ = "Bob Mottram" __license__ = "AGPL3+" __version__ = "1.3.0" __maintainer__ = "Bob Mottram" __email__ = "bob@libreserver.org" __status__ = "Production" __module_group__ = "Web Interface" import os from pprint import pprint from webfinger import webfinger_handle from utils import standardize_text from utils import get_display_name from utils import is_group_account from utils import has_object_dict from utils import get_occupation_name from utils import get_locked_account from utils import get_full_domain from utils import is_artist from utils import is_dormant from utils import get_nickname_from_actor from utils import get_domain_from_actor from utils import is_system_account from utils import remove_html from utils import load_json from utils import get_config_param from utils import get_image_formats from utils import acct_dir from utils import get_supported_languages from utils import local_actor_url from utils import get_reply_interval_hours from utils import get_account_timezone from utils import remove_eol from languages import get_actor_languages from skills import get_skills from theme import get_themes_list from person import person_box_json from person import get_actor_json from person import get_person_avatar_url from posts import get_post_expiry_keep_dms from posts import get_post_expiry_days from posts import get_person_box from posts import is_moderator from posts import parse_user_feed from posts import is_create_inside_announce from donate import get_donation_url from donate import get_website from xmpp import get_xmpp_address from matrix import get_matrix_address from ssb import get_ssb_address from pgp import get_email_address from pgp import get_pgp_fingerprint from pgp import get_pgp_pub_key from enigma import get_enigma_pub_key from tox import get_tox_address from briar import get_briar_address from cwtch import get_cwtch_address from filters import is_filtered from follow import is_follower_of_person from follow import get_follower_domains from webapp_frontscreen import html_front_screen from webapp_utils import edit_number_field from webapp_utils import html_keyboard_navigation from webapp_utils import html_hide_from_screen_reader from webapp_utils import scheduled_posts_exist from webapp_utils import html_header_with_external_style from webapp_utils import html_header_with_person_markup from webapp_utils import html_footer from webapp_utils import add_emoji_to_display_name from webapp_utils import get_profile_background_file from webapp_utils import html_post_separator from webapp_utils import edit_check_box from webapp_utils import edit_text_field from webapp_utils import edit_text_area from webapp_utils import begin_edit_section from webapp_utils import end_edit_section from blog import get_blog_address from webapp_post import individual_post_as_html from webapp_timeline import html_individual_share from webapp_timeline import page_number_buttons from blocking import get_cw_list_variable from blocking import is_blocked from content import bold_reading_string from roles import is_devops THEME_FORMATS = '.zip, .gz' def _valid_profile_preview_post(post_json_object: {}, person_url: str) -> (bool, {}): """Returns true if the given post should appear on a person/group profile after searching for a handle """ is_announced_feed_item = False if is_create_inside_announce(post_json_object): is_announced_feed_item = True post_json_object = post_json_object['object'] if not post_json_object.get('type'): return False, None if post_json_object['type'] == 'Create': if not has_object_dict(post_json_object): return False, None if post_json_object['type'] != 'Create' and \ post_json_object['type'] != 'Announce': if post_json_object['type'] != 'Note' and \ post_json_object['type'] != 'Page': return False, None if not post_json_object.get('to'): return False, None if not post_json_object.get('id'): return False, None # wrap in create cc_list = [] if post_json_object.get('cc'): cc_list = post_json_object['cc'] new_post_json_object = { 'object': post_json_object, 'to': post_json_object['to'], 'cc': cc_list, 'id': post_json_object['id'], 'actor': person_url, 'type': 'Create' } post_json_object = new_post_json_object if not post_json_object.get('actor'): return False, None if not is_announced_feed_item: if has_object_dict(post_json_object): if post_json_object['actor'] != person_url and \ post_json_object['object']['type'] != 'Page': return False, None return True, post_json_object def html_profile_after_search(recent_posts_cache: {}, max_recent_posts: int, translate: {}, base_dir: str, path: str, http_prefix: str, nickname: str, domain: str, port: int, profile_handle: str, session, cached_webfingers: {}, person_cache: {}, debug: bool, project_version: str, yt_replace_domain: str, twitter_replacement_domain: str, show_published_date_only: bool, default_timeline: str, peertube_instances: [], allow_local_network_access: bool, theme_name: str, access_keys: {}, system_language: str, max_like_count: int, signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, timezone: str, onion_domain: str, i2p_domain: str, bold_reading: bool, dogwhistles: {}) -> str: """Show a profile page after a search for a fediverse address """ http = False gnunet = False ipfs = False ipns = False if http_prefix == 'http': http = True elif http_prefix == 'gnunet': gnunet = True elif http_prefix == 'ipfs': ipfs = True elif http_prefix == 'ipns': ipns = True from_domain = domain if onion_domain: if '.onion/' in profile_handle or profile_handle.endswith('.onion'): from_domain = onion_domain http = True if i2p_domain: if '.i2p/' in profile_handle or profile_handle.endswith('.i2p'): from_domain = i2p_domain http = True profile_json, as_header = \ get_actor_json(from_domain, profile_handle, http, gnunet, ipfs, ipns, debug, False, signing_priv_key_pem, session) if not profile_json: return None person_url = profile_json['id'] search_domain, search_port = get_domain_from_actor(person_url) if not search_domain: return None search_nickname = get_nickname_from_actor(person_url) if not search_nickname: return None search_domain_full = get_full_domain(search_domain, search_port) profile_str = '' css_filename = base_dir + '/epicyon-profile.css' if os.path.isfile(base_dir + '/epicyon.css'): css_filename = base_dir + '/epicyon.css' is_group = False if profile_json.get('type'): if profile_json['type'] == 'Group': is_group = True avatar_url = '' if profile_json.get('icon'): if profile_json['icon'].get('url'): avatar_url = profile_json['icon']['url'] if not avatar_url: avatar_url = get_person_avatar_url(base_dir, person_url, person_cache) display_name = search_nickname if profile_json.get('name'): display_name = profile_json['name'] locked_account = get_locked_account(profile_json) if locked_account: display_name += '🔒' moved_to = '' if profile_json.get('movedTo'): moved_to = profile_json['movedTo'] if '"' in moved_to: moved_to = moved_to.split('"')[1] display_name += ' ⌂' follows_you = \ is_follower_of_person(base_dir, nickname, domain, search_nickname, search_domain_full) profile_description = '' if profile_json.get('summary'): profile_description = profile_json['summary'] outbox_url = None if not profile_json.get('outbox'): if debug: pprint(profile_json) print('DEBUG: No outbox found') return None outbox_url = profile_json['outbox'] # profileBackgroundImage = '' # if profile_json.get('image'): # if profile_json['image'].get('url'): # profileBackgroundImage = profile_json['image']['url'] # url to return to back_url = path if not back_url.endswith('/inbox'): back_url += '/inbox' profile_description_short = profile_description if '\n' in profile_description: if len(profile_description.split('\n')) > 2: profile_description_short = '' else: if '
' in profile_description: if len(profile_description.split('
')) > 2: profile_description_short = '' # keep the profile description short if len(profile_description_short) > 2048: profile_description_short = '' # remove formatting from profile description used on title avatar_description = '' if profile_json.get('summary'): if isinstance(profile_json['summary'], str): avatar_description = \ profile_json['summary'].replace('
', '\n') avatar_description = avatar_description.replace('

', '') avatar_description = avatar_description.replace('

', '') if '<' in avatar_description: avatar_description = remove_html(avatar_description) image_url = '' if profile_json.get('image'): if profile_json['image'].get('url'): image_url = profile_json['image']['url'] also_known_as = None if profile_json.get('alsoKnownAs'): also_known_as = profile_json['alsoKnownAs'] joined_date = None if profile_json.get('published'): if 'T' in profile_json['published']: joined_date = profile_json['published'] profile_str = \ _get_profile_header_after_search(nickname, default_timeline, search_nickname, search_domain_full, translate, display_name, follows_you, profile_description_short, avatar_url, image_url, moved_to, profile_json['id'], also_known_as, access_keys, joined_date) domain_full = get_full_domain(domain, port) follow_is_permitted = True if not profile_json.get('followers'): # no followers collection specified within actor follow_is_permitted = False elif search_nickname == 'news' and search_domain_full == domain_full: # currently the news actor is not something you can follow follow_is_permitted = False elif search_nickname == nickname and search_domain_full == domain_full: # don't follow yourself! follow_is_permitted = False blocked = \ is_blocked(base_dir, nickname, domain, search_nickname, search_domain) if follow_is_permitted: follow_str = 'Follow' if is_group: follow_str = 'Join' profile_str += \ '
\n' + \ '
\n' + \ '
\n' profile_str += \ ' \n' + \ ' \n' profile_str += \ ' \n' if blocked: profile_str += \ ' \n' profile_str += \ '
\n' + \ '
\n' + \ '
\n' else: profile_str += \ '
\n' + \ '
\n' + \ '
\n' + \ ' \n' + \ ' \n' + \ '
\n' + \ '
\n' + \ '
\n' user_feed = \ parse_user_feed(signing_priv_key_pem, session, outbox_url, as_header, project_version, http_prefix, from_domain, debug) if user_feed: i = 0 for item in user_feed: show_item, post_json_object = \ _valid_profile_preview_post(item, person_url) if not show_item: continue profile_str += \ individual_post_as_html(signing_priv_key_pem, True, recent_posts_cache, max_recent_posts, translate, None, base_dir, session, cached_webfingers, person_cache, nickname, domain, port, post_json_object, avatar_url, False, False, http_prefix, project_version, 'inbox', yt_replace_domain, twitter_replacement_domain, show_published_date_only, peertube_instances, allow_local_network_access, theme_name, system_language, max_like_count, False, False, False, False, False, False, cw_lists, lists_enabled, timezone, False, bold_reading, dogwhistles) i += 1 if i >= 8: break instance_title = get_config_param(base_dir, 'instanceTitle') return html_header_with_external_style(css_filename, instance_title, None) + \ profile_str + html_footer() def _get_profile_header(base_dir: str, http_prefix: str, nickname: str, domain: str, domain_full: str, translate: {}, default_timeline: str, display_name: str, profile_description_short: str, login_button: str, avatar_url: str, theme: str, moved_to: str, also_known_as: [], pinned_content: str, access_keys: {}, joined_date: str, occupation_name: str) -> str: """The header of the profile screen, containing background image and avatar """ banner_file, _ = \ get_profile_background_file(base_dir, nickname, domain, theme) html_str = \ '\n\n
\n' + \ ' \n' + \ ' \n' + \ '
\n' + \ ' \n' + \ ' \n' occupation_str = '' if occupation_name: occupation_str += \ ' ' + occupation_name + '
\n' html_str += '

' + display_name + '

\n' + occupation_str html_str += \ '

@' + nickname + '@' + domain_full + '
\n' if joined_date: html_str += \ '

' + translate['Joined'] + ' ' + \ joined_date.split('T')[0] + '
\n' if moved_to: new_nickname = get_nickname_from_actor(moved_to) new_domain, new_port = get_domain_from_actor(moved_to) new_domain_full = get_full_domain(new_domain, new_port) if new_nickname and new_domain: html_str += \ '

' + translate['New account'] + ': ' + \ '@' + \ new_nickname + '@' + new_domain_full + '
\n' elif also_known_as: other_accounts_html = \ '

' + translate['Other accounts'] + ': ' actor = local_actor_url(http_prefix, nickname, domain_full) ctr = 0 if isinstance(also_known_as, list): for alt_actor in also_known_as: if alt_actor == actor: continue if ctr > 0: other_accounts_html += ' ' ctr += 1 alt_domain, _ = get_domain_from_actor(alt_actor) other_accounts_html += \ '' + alt_domain + '' elif isinstance(also_known_as, str): if also_known_as != actor: ctr += 1 alt_domain, _ = get_domain_from_actor(also_known_as) other_accounts_html += \ '' + alt_domain + '' other_accounts_html += '

\n' if ctr > 0: html_str += other_accounts_html html_str += \ ' ' + \ '' + translate['QR Code'] + \
        '

\n' + \ '

' + profile_description_short + '

\n' + login_button if pinned_content: html_str += pinned_content.replace('

', '

📎', 1) # show vcard download link html_str += \ ' ' + \ 'vCard\n' html_str += \ '

\n' + \ '
\n\n' return html_str def _get_profile_header_after_search(nickname: str, default_timeline: str, search_nickname: str, search_domain_full: str, translate: {}, display_name: str, follows_you: bool, profile_description_short: str, avatar_url: str, image_url: str, moved_to: str, actor: str, also_known_as: [], access_keys: {}, joined_date: str) -> str: """The header of a searched for handle, containing background image and avatar """ if not image_url: image_url = '/defaultprofilebackground' html_str = \ '\n\n
\n' + \ ' \n' + \ ' \n' + \ '
\n' if avatar_url: html_str += \ ' \n' + \ ' \n' if not display_name: display_name = search_nickname html_str += \ '

' + display_name + '

\n' + \ '

@' + search_nickname + '@' + search_domain_full + \ '
\n' if joined_date: html_str += '

' + translate['Joined'] + ' ' + \ joined_date.split('T')[0] + '

\n' if follows_you: html_str += '

' + translate['Follows you'] + '

\n' if moved_to: new_nickname = get_nickname_from_actor(moved_to) new_domain, new_port = get_domain_from_actor(moved_to) new_domain_full = get_full_domain(new_domain, new_port) if new_nickname and new_domain: new_handle = new_nickname + '@' + new_domain_full html_str += '

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

\n' elif also_known_as: other_accounts_html = \ '

' + translate['Other accounts'] + ': ' ctr = 0 if isinstance(also_known_as, list): for alt_actor in also_known_as: if alt_actor == actor: continue if ctr > 0: other_accounts_html += ' ' ctr += 1 alt_domain, _ = get_domain_from_actor(alt_actor) other_accounts_html += \ '' + alt_domain + '' elif isinstance(also_known_as, str): if also_known_as != actor: ctr += 1 alt_domain, _ = get_domain_from_actor(also_known_as) other_accounts_html += \ '' + alt_domain + '' other_accounts_html += '

\n' if ctr > 0: html_str += other_accounts_html html_str += \ '

' + profile_description_short + '

\n' + \ '
\n' + \ '
\n\n' return html_str def html_profile(signing_priv_key_pem: str, rss_icon_at_top: bool, icons_as_buttons: bool, default_timeline: str, recent_posts_cache: {}, max_recent_posts: int, translate: {}, project_version: str, base_dir: str, http_prefix: str, authorized: bool, profile_json: {}, selected: str, session, cached_webfingers: {}, person_cache: {}, yt_replace_domain: str, twitter_replacement_domain: str, show_published_date_only: bool, newswire: {}, theme: str, dormant_months: int, peertube_instances: [], allow_local_network_access: bool, text_mode_banner: str, debug: bool, access_keys: {}, city: str, system_language: str, max_like_count: int, shared_items_federated_domains: [], extra_json: {}, page_number: int, max_items_per_page: int, cw_lists: {}, lists_enabled: str, content_license_url: str, timezone: str, bold_reading: bool) -> str: """Show the profile page as html """ nickname = profile_json['preferredUsername'] if not nickname: return "" if is_system_account(nickname): return html_front_screen(signing_priv_key_pem, rss_icon_at_top, icons_as_buttons, default_timeline, recent_posts_cache, max_recent_posts, translate, project_version, base_dir, http_prefix, authorized, profile_json, selected, session, cached_webfingers, person_cache, yt_replace_domain, twitter_replacement_domain, show_published_date_only, newswire, theme, extra_json, allow_local_network_access, access_keys, system_language, max_like_count, shared_items_federated_domains, None, page_number, max_items_per_page, cw_lists, lists_enabled, {}) domain, port = get_domain_from_actor(profile_json['id']) if not domain: return "" display_name = \ add_emoji_to_display_name(session, base_dir, http_prefix, nickname, domain, profile_json['name'], True, translate) domain_full = get_full_domain(domain, port) profile_description = \ add_emoji_to_display_name(session, base_dir, http_prefix, nickname, domain, profile_json['summary'], False, translate) if profile_description: profile_description = standardize_text(profile_description) posts_button = 'button' following_button = 'button' followers_button = 'button' roles_button = 'button' skills_button = 'button' # shares_button = 'button' # wanted_button = 'button' if selected == 'posts': posts_button = 'buttonselected' elif selected == 'following': following_button = 'buttonselected' elif selected == 'followers': followers_button = 'buttonselected' elif selected == 'roles': roles_button = 'buttonselected' elif selected == 'skills': skills_button = 'buttonselected' # elif selected == 'shares': # shares_button = 'buttonselected' # elif selected == 'wanted': # wanted_button = 'buttonselected' login_button = '' follow_approvals_section = '' follow_approvals = False edit_profile_str = '' logout_str = '' actor = profile_json['id'] users_path = '/users/' + actor.split('/users/')[1] donate_section = '' donate_url = get_donation_url(profile_json) website_url = get_website(profile_json, translate) blog_address = get_blog_address(profile_json) enigma_pub_key = get_enigma_pub_key(profile_json) pgp_pub_key = get_pgp_pub_key(profile_json) pgp_fingerprint = get_pgp_fingerprint(profile_json) email_address = get_email_address(profile_json) xmpp_address = get_xmpp_address(profile_json) matrix_address = get_matrix_address(profile_json) ssb_address = get_ssb_address(profile_json) tox_address = get_tox_address(profile_json) briar_address = get_briar_address(profile_json) cwtch_address = get_cwtch_address(profile_json) if donate_url or website_url or xmpp_address or matrix_address or \ ssb_address or tox_address or briar_address or \ cwtch_address or pgp_pub_key or enigma_pub_key or \ pgp_fingerprint or email_address: donate_section = '
\n' donate_section += '
\n' if donate_url and not is_system_account(nickname): donate_section += \ '

' + \ '

\n' if website_url: donate_section += \ '

' + translate['Website'] + ': ' + website_url + '

\n' if email_address: donate_section += \ '

' + translate['Email'] + ': ' + \ email_address + '

\n' if blog_address: donate_section += \ '

Blog: ' + blog_address + '

\n' if xmpp_address: donate_section += \ '

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

\n' if matrix_address: donate_section += \ '

' + translate['Matrix'] + ': ' + matrix_address + '

\n' if ssb_address: donate_section += \ '

SSB:

\n' if tox_address: donate_section += \ '

Tox:

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

\n' else: donate_section += \ '

briar://

\n' if cwtch_address: donate_section += \ '

Cwtch:

\n' if enigma_pub_key: donate_section += \ '

Enigma:

\n' if pgp_fingerprint: donate_section += \ '

PGP: ' + \ pgp_fingerprint.replace('\n', '
') + '

\n' if pgp_pub_key: donate_section += \ '

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

\n' donate_section += '
\n' donate_section += '
\n' if authorized: edit_profile_str = \ '' + \ '| ' + translate['Edit'] + '\n' logout_str = \ '' + \ '| ' + translate['Logout'] + \
            '\n' # are there any follow requests? follow_requests_filename = \ acct_dir(base_dir, nickname, domain) + '/followrequests.txt' if os.path.isfile(follow_requests_filename): with open(follow_requests_filename, 'r', encoding='utf-8') as foll_file: for line in foll_file: if len(line) > 0: follow_approvals = True followers_button = 'buttonhighlighted' if selected == 'followers': followers_button = 'buttonselectedhighlighted' break if selected == 'followers': if follow_approvals: curr_follower_domains = \ get_follower_domains(base_dir, nickname, domain) with open(follow_requests_filename, 'r', encoding='utf-8') as req_file: for follower_handle in req_file: if len(follower_handle) > 0: follower_handle = \ remove_eol(follower_handle) if '://' in follower_handle: follower_actor = follower_handle else: nick = follower_handle.split('@')[0] dom = follower_handle.split('@')[1] follower_actor = \ local_actor_url(http_prefix, nick, dom) # is this a new domain? # if so then append a new instance indicator follower_domain, _ = \ get_domain_from_actor(follower_actor) new_follower_domain = '' if follower_domain not in curr_follower_domains: new_follower_domain = ' ✨' base_path = '/users/' + nickname follow_approvals_section += \ '
' follow_approvals_section += \ '' follow_approvals_section += \ '' + \ follower_handle + \ new_follower_domain + '' # show Approve and Deny buttons follow_approvals_section += \ '' follow_approvals_section += \ '

' follow_approvals_section += \ '' follow_approvals_section += \ '' follow_approvals_section += '
' profile_description_short = profile_description if '\n' in profile_description: if len(profile_description.split('\n')) > 2: profile_description_short = '' else: if '
' in profile_description: if len(profile_description.split('
')) > 2: profile_description_short = '' profile_description = profile_description.replace('
', '\n') # keep the profile description short if len(profile_description_short) > 2048: profile_description_short = '' # remove formatting from profile description used on title avatar_description = '' if profile_json.get('summary'): avatar_description = profile_json['summary'].replace('
', '\n') avatar_description = avatar_description.replace('

', '') avatar_description = avatar_description.replace('

', '') moved_to = '' if profile_json.get('movedTo'): moved_to = profile_json['movedTo'] if '"' in moved_to: moved_to = moved_to.split('"')[1] also_known_as = None if profile_json.get('alsoKnownAs'): also_known_as = profile_json['alsoKnownAs'] joined_date = None if profile_json.get('published'): if 'T' in profile_json['published']: joined_date = profile_json['published'] occupation_name = None if profile_json.get('hasOccupation'): occupation_name = get_occupation_name(profile_json) avatar_url = profile_json['icon']['url'] # use alternate path for local avatars to avoid any caching issues if '://' + domain_full + '/system/accounts/avatars/' in avatar_url: avatar_url = \ avatar_url.replace('://' + domain_full + '/system/accounts/avatars/', '://' + domain_full + '/users/') # get pinned post content account_dir = acct_dir(base_dir, nickname, domain) pinned_filename = account_dir + '/pinToProfile.txt' pinned_content = None if os.path.isfile(pinned_filename): with open(pinned_filename, 'r', encoding='utf-8') as pin_file: pinned_content = pin_file.read() profile_header_str = \ _get_profile_header(base_dir, http_prefix, nickname, domain, domain_full, translate, default_timeline, display_name, profile_description_short, login_button, avatar_url, theme, moved_to, also_known_as, pinned_content, access_keys, joined_date, occupation_name) # keyboard navigation user_path_str = '/users/' + nickname deft = default_timeline is_group = False followers_str = translate['Followers'] if is_group_account(base_dir, nickname, domain): is_group = True followers_str = translate['Members'] menu_timeline = \ html_hide_from_screen_reader('🏠') + ' ' + \ translate['Switch to timeline view'] menu_edit = \ html_hide_from_screen_reader('✍') + ' ' + translate['Edit'] menu_followers = \ html_hide_from_screen_reader('👪') + ' ' + followers_str 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 is_group: menu_following = \ html_hide_from_screen_reader('👥') + ' ' + translate['Following'] nav_links[menu_following] = user_path_str + '/following#timeline' menu_roles = \ html_hide_from_screen_reader('🤚') + ' ' + translate['Roles'] nav_links[menu_roles] = user_path_str + '/roles#timeline' menu_skills = \ html_hide_from_screen_reader('🛠') + ' ' + translate['Skills'] nav_links[menu_skills] = user_path_str + '/skills#timeline' if is_artist(base_dir, nickname): menu_theme_designer = \ html_hide_from_screen_reader('🎨') + ' ' + \ translate['Theme Designer'] nav_links[menu_theme_designer] = user_path_str + '/themedesigner' nav_access_keys = {} for variable_name, key in access_keys.items(): if not locals().get(variable_name): continue nav_access_keys[locals()[variable_name]] = key profile_str = html_keyboard_navigation(text_mode_banner, nav_links, nav_access_keys) profile_str += profile_header_str + donate_section profile_str += '
\n' profile_str += '
' profile_str += \ ' ' + \ '' if not is_group: profile_str += \ ' ' + \ '' profile_str += \ ' ' + \ '' if not is_group: profile_str += \ ' ' + \ '' profile_str += \ ' ' + \ '' # profile_str += \ # ' ' + \ # '' # profile_str += \ # ' ' + \ # '' profile_str += logout_str + edit_profile_str profile_str += '
' profile_str += '
' # start of #timeline profile_str += '
\n' profile_str += follow_approvals_section css_filename = base_dir + '/epicyon-profile.css' if os.path.isfile(base_dir + '/epicyon.css'): css_filename = base_dir + '/epicyon.css' license_str = \ '' + \ '' + \
        translate['Get the source code'] + '' if selected == 'posts': profile_str += \ _html_profile_posts(recent_posts_cache, max_recent_posts, translate, base_dir, http_prefix, authorized, nickname, domain, port, session, cached_webfingers, person_cache, project_version, yt_replace_domain, twitter_replacement_domain, show_published_date_only, peertube_instances, allow_local_network_access, theme, system_language, max_like_count, signing_priv_key_pem, cw_lists, lists_enabled, timezone, bold_reading, {}) + license_str if not is_group: if selected == 'following': profile_str += \ _html_profile_following(translate, base_dir, http_prefix, authorized, nickname, domain, session, cached_webfingers, person_cache, extra_json, project_version, ["unfollow"], 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, authorized, nickname, domain, session, cached_webfingers, person_cache, extra_json, project_version, ["block"], selected, users_path, page_number, max_items_per_page, dormant_months, debug, signing_priv_key_pem) if not is_group: if selected == 'roles': profile_str += \ _html_profile_roles(translate, nickname, domain_full, extra_json) elif selected == 'skills': profile_str += \ _html_profile_skills(extra_json) # elif selected == 'shares': # profile_str += \ # _html_profile_shares(actor, translate, # domain_full, # extra_json, 'shares') + license_str # elif selected == 'wanted': # profile_str += \ # _html_profile_shares(actor, translate, # domain_full, # extra_json, 'wanted') + license_str # end of #timeline profile_str += '
' instance_title = \ get_config_param(base_dir, 'instanceTitle') profile_str = \ html_header_with_person_markup(css_filename, instance_title, profile_json, city, content_license_url) + \ profile_str + html_footer() return profile_str def _html_profile_posts(recent_posts_cache: {}, max_recent_posts: int, translate: {}, base_dir: str, http_prefix: str, authorized: bool, nickname: str, domain: str, port: int, session, cached_webfingers: {}, person_cache: {}, project_version: str, yt_replace_domain: str, twitter_replacement_domain: str, show_published_date_only: bool, peertube_instances: [], allow_local_network_access: bool, theme_name: str, system_language: str, max_like_count: int, signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, timezone: str, bold_reading: bool, dogwhistles: {}) -> str: """Shows posts on the profile screen These should only be public posts """ separator_str = html_post_separator(base_dir, None) profile_str = '' max_items = 4 ctr = 0 curr_page = 1 box_name = 'outbox' while ctr < max_items and curr_page < 4: outbox_feed_path_str = \ '/users/' + nickname + '/' + box_name + '?page=' + \ str(curr_page) outbox_feed = \ person_box_json({}, session, base_dir, domain, port, outbox_feed_path_str, http_prefix, 10, box_name, authorized, 0, False, 0) if not outbox_feed: break if len(outbox_feed['orderedItems']) == 0: break for item in outbox_feed['orderedItems']: if item['type'] == 'Create': post_str = \ individual_post_as_html(signing_priv_key_pem, True, recent_posts_cache, max_recent_posts, translate, None, base_dir, session, cached_webfingers, person_cache, nickname, domain, port, item, None, True, False, http_prefix, project_version, 'inbox', yt_replace_domain, twitter_replacement_domain, show_published_date_only, peertube_instances, allow_local_network_access, theme_name, system_language, max_like_count, False, False, False, True, False, False, cw_lists, lists_enabled, timezone, False, bold_reading, dogwhistles) if post_str: profile_str += post_str + separator_str ctr += 1 if ctr >= max_items: break curr_page += 1 return profile_str def _html_profile_following(translate: {}, base_dir: str, http_prefix: str, authorized: bool, nickname: str, domain: str, session, cached_webfingers: {}, person_cache: {}, following_json: {}, project_version: str, buttons: [], feed_name: str, actor: str, page_number: int, max_items_per_page: int, dormant_months: int, debug: bool, signing_priv_key_pem: str) -> str: """Shows following on the profile screen """ profile_str = '' if authorized and page_number: if authorized and page_number > 1: # page up arrow profile_str += \ '
\n' + \ ' ' + \
                translate['Page up'] + '\n' + \ '
\n' for following_actor in following_json['orderedItems']: # is this a dormant followed account? dormant = False if authorized and feed_name == 'following': dormant = \ is_dormant(base_dir, nickname, domain, following_actor, dormant_months) profile_str += \ _individual_follow_as_html(signing_priv_key_pem, translate, base_dir, session, cached_webfingers, person_cache, domain, following_actor, authorized, nickname, http_prefix, project_version, dormant, debug, buttons) if authorized and max_items_per_page and page_number: if len(following_json['orderedItems']) >= max_items_per_page: # page down arrow profile_str += \ '
\n' + \ ' ' + \
                translate['Page down'] + '\n' + \ '
\n' # list of page numbers profile_str += \ page_number_buttons(actor, feed_name, page_number, 'buttonheader') # some vertical padding to allow "finger space" on mobile profile_str += '
' return profile_str def _html_profile_roles(translate: {}, nickname: str, domain: str, roles_list: []) -> str: """Shows roles on the profile screen """ profile_str = '' profile_str += \ '
\n
\n' for role in roles_list: if translate.get(role): profile_str += '

' + translate[role] + '

\n' else: profile_str += '

' + role + '

\n' profile_str += '
\n' if len(profile_str) == 0: profile_str += \ '

@' + nickname + '@' + domain + ' has no roles assigned

\n' else: profile_str = '
' + profile_str + '
\n' return profile_str def _html_profile_skills(skills_json: {}) -> str: """Shows skills on the profile screen """ profile_str = '' for skill, level in skills_json.items(): profile_str += \ '
' + skill + \ '
\n
\n' if len(profile_str) > 0: profile_str = '
' + \ profile_str + '
\n' return profile_str def _html_profile_shares(actor: str, translate: {}, domain: str, shares_json: {}, shares_file_type: str) -> str: """Shows shares on the profile screen """ profile_str = '' for item in shares_json['orderedItems']: profile_str += html_individual_share(domain, item['shareId'], actor, item, translate, False, False, shares_file_type) if len(profile_str) > 0: profile_str = '
' + profile_str + '
\n' return profile_str def _grayscale_enabled(base_dir: str) -> bool: """Is grayscale UI enabled? """ return os.path.isfile(base_dir + '/accounts/.grayscale') def _html_themes_dropdown(base_dir: str, translate: {}) -> str: """Returns the html for theme selection dropdown """ # Themes section themes = get_themes_list(base_dir) themes_dropdown = '
\n' grayscale = _grayscale_enabled(base_dir) themes_dropdown += \ edit_check_box(translate['Grayscale'], 'grayscale', grayscale) dyslexic_font = get_config_param(base_dir, 'dyslexicFont') themes_dropdown += \ edit_check_box(translate['Dyslexic font'], 'dyslexicFont', dyslexic_font) themes_dropdown += '
' if os.path.isfile(base_dir + '/fonts/custom.woff') or \ os.path.isfile(base_dir + '/fonts/custom.woff2') or \ os.path.isfile(base_dir + '/fonts/custom.otf') or \ os.path.isfile(base_dir + '/fonts/custom.ttf'): themes_dropdown += \ edit_check_box(translate['Remove the custom font'], 'removeCustomFont', False) theme_name = get_config_param(base_dir, 'theme') themes_dropdown = \ themes_dropdown.replace('