__filename__ = "webapp_profile.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.6.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 flags import is_dormant
from flags import is_artist
from flags import is_system_account
from flags import is_group_account
from flags import is_valid_date
from flags import is_premium_account
from utils import get_person_icon
from utils import text_mode_removals
from utils import replace_strings
from utils import data_dir
from utils import time_days_ago
from utils import uninvert_text
from utils import get_attributed_to
from utils import get_url_from_post
from utils import get_memorials
from utils import text_in_file
from utils import dangerous_markup
from utils import ap_proxy_type
from utils import remove_id_ending
from utils import standardize_text
from utils import get_display_name
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 get_nickname_from_actor
from utils import get_domain_from_actor
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 utils import get_actor_from_post
from utils import resembles_url
from languages import get_actor_languages
from skills import get_skills
from theme import get_themes_list
from person import get_featured_hashtags_as_html
from person import get_featured_hashtags
from person import person_box_json
from person import get_actor_json
from person import get_person_avatar_url
from person import get_person_notes
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 posts import get_max_profile_posts
from website import get_website
from website import get_gemini_link
from donate import get_donation_url
from pronouns import get_pronouns
from pixelfed import get_pixelfed
from discord import get_discord
from art import get_art_site_url
from music import get_music_site_url
from youtube import get_youtube
from peertube import get_peertube
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_deltachat_invite
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 follow import is_following_actor
from webapp_frontscreen import html_front_screen
from webapp_utils import text_mode_browser
from webapp_utils import html_following_dropdown
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 blog import account_has_blog
from webapp_post import individual_post_as_html
from webapp_timeline import html_individual_share
from webapp_timeline import page_number_buttons
from cwlists import get_cw_list_variable
from blocking import get_account_blocks
from blocking import is_blocked
from blocking import sending_is_blocked2
from content import remove_link_trackers_from_content
from content import bold_reading_string
from roles import is_devops
from session import get_json_valid
from session import site_is_verified
from session import get_json
from shares import actor_attached_shares_as_html
from git import get_repo_url
from reading import html_profile_book_list
THEME_FORMATS = '.zip, .gz'
BLOCKFILE_FORMATS = '.csv'
def _import_export_blocks(translate: {}) -> str:
    """ import and export blocks on edit profile screen
    """
    edit_profile_form = \
        '      ' + \
        translate['Import Blocks'] + ' \n'
    edit_profile_form += '      ' + \
        translate['Export Blocks'] + ' ➤ 
', '')
            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'):
            url_str = get_url_from_post(profile_json['image']['url'])
            image_url = remove_html(url_str)
    also_known_as = None
    if profile_json.get('alsoKnownAs'):
        also_known_as = remove_html(profile_json['alsoKnownAs'])
    elif profile_json.get('sameAs'):
        also_known_as = remove_html(profile_json['sameAs'])
    joined_date = None
    if profile_json.get('published'):
        if 'T' in profile_json['published']:
            joined_date = remove_html(profile_json['published'])
    actor_proxied = ap_proxy_type(profile_json)
    website_url = get_website(profile_json, translate)
    repo_url = get_repo_url(profile_json)
    # is sending posts to this account blocked?
    send_blocks_str = ''
    if sending_is_blocked2(base_dir, nickname, domain,
                           search_domain_full, person_url):
        send_block_filename = \
            acct_dir(base_dir, nickname, domain) + '/send_blocks.txt'
        if text_in_file(person_url,
                        send_block_filename, False):
            send_blocks_str = translate['FollowAccountWarning']
        elif text_in_file('://' + search_domain_full + '\n',
                          send_block_filename, False):
            send_blocks_str = translate['FollowWarning']
    birth_date = ''
    if profile_json.get('vcard:bday'):
        birth_date = profile_json['vcard:bday']
    profile_str = \
        _get_profile_header_after_search(base_dir, nickname, domain,
                                         default_timeline,
                                         search_nickname,
                                         search_domain_full,
                                         translate,
                                         display_name, pronouns,
                                         you_follow, follows_you,
                                         profile_description_short,
                                         featured_hashtags,
                                         avatar_url, image_url,
                                         moved_to, profile_json['id'],
                                         also_known_as, access_keys,
                                         joined_date, actor_proxied,
                                         attached_shared_items,
                                         website_url, repo_url,
                                         send_blocks_str,
                                         authorized,
                                         person_url, no_of_books,
                                         birth_date,
                                         youtube, peertube, pixelfed,
                                         discord, music_site_url,
                                         art_site_url)
    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,
                   None, None)
    if follow_is_permitted:
        follow_str = 'Follow'
        if is_group:
            follow_str = 'Join'
        profile_str += \
            '\n' + \
            '  \n' + \
            '
\n'
    else:
        profile_str += \
            '\n' + \
            '  \n' + \
            '
\n'
    text_mode_separator = '
' + translate['Shares'] + ': 
\n'
def _get_profile_header(base_dir: str, http_prefix: str, nickname: str,
                        domain: str, domain_full: str, translate: {},
                        default_timeline: str,
                        display_name: str, pronouns: str,
                        profile_description_short: str,
                        featured_hashtags: str,
                        login_button: str, avatar_url: str,
                        theme: str, moved_to: str,
                        also_known_as: [],
                        pinned_content: str,
                        attached_shared_items: str,
                        access_keys: {},
                        joined_date: str,
                        occupation_name: str,
                        actor_proxied: str,
                        person_url: str,
                        no_of_books: int,
                        authorized: bool,
                        birth_date: str,
                        premium: bool) -> 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'
    # book events for this actor
    html_str += html_profile_book_list(base_dir, person_url, no_of_books,
                                       translate, nickname, domain,
                                       authorized)
    return html_str
def _get_profile_header_after_search(base_dir: str,
                                     nickname: str, domain: str,
                                     default_timeline: str,
                                     search_nickname: str,
                                     search_domain_full: str,
                                     translate: {},
                                     display_name: str,
                                     pronouns: str,
                                     you_follow: bool,
                                     follows_you: bool,
                                     profile_description_short: str,
                                     featured_hashtags: str,
                                     avatar_url: str, image_url: str,
                                     moved_to: str, actor: str,
                                     also_known_as: [],
                                     access_keys: {},
                                     joined_date: str,
                                     actor_proxied: str,
                                     attached_shared_items: str,
                                     website_url: str,
                                     repo_url: str,
                                     send_blocks_str: str,
                                     authorized: bool,
                                     person_url: str,
                                     no_of_books: str,
                                     birth_date: str,
                                     youtube: str,
                                     peertube: str,
                                     pixelfed: str,
                                     discord: str,
                                     music_site_url: str,
                                     art_site_url: 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'
    if attached_shared_items:
        html_str += \
            _profile_shared_items_list(attached_shared_items,
                                       translate)
    # book events for this actor
    html_str += html_profile_book_list(base_dir, person_url, no_of_books,
                                       translate,
                                       nickname, domain, authorized)
    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,
                 buy_sites: {},
                 actor_proxied: str,
                 max_shares_on_profile: int,
                 sites_unavailable: [],
                 no_of_books: int,
                 auto_cw_cache: {},
                 known_epicyon_instances: [],
                 mitm_servers: [],
                 instance_software: {},
                 hide_recent_posts: {}) -> str:
    """Show the profile page as html
    """
    show_moved_accounts = False
    if authorized:
        moved_accounts_filename = data_dir(base_dir) + '/actors_moved.txt'
        if os.path.isfile(moved_accounts_filename):
            show_moved_accounts = True
    nickname = profile_json['preferredUsername']
    if not nickname:
        return ""
    if is_system_account(nickname):
        min_images_for_accounts: list[str] = []
        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,
                                 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, cw_lists,
                                 lists_enabled, {},
                                 min_images_for_accounts, buy_sites,
                                 auto_cw_cache,
                                 known_epicyon_instances,
                                 mitm_servers, instance_software)
    domain, port = get_domain_from_actor(profile_json['id'])
    if not domain:
        return ""
    display_name = remove_html(profile_json['name'])
    display_name = standardize_text(display_name)
    display_name = \
        add_emoji_to_display_name(session, base_dir, http_prefix,
                                  nickname, domain,
                                  display_name, False, translate)
    domain_full = get_full_domain(domain, port)
    if not dangerous_markup(profile_json['summary'], False, []):
        profile_description = profile_json['summary']
    else:
        profile_description = remove_html(profile_json['summary'])
    profile_description = \
        remove_link_trackers_from_content(profile_description)
    profile_description = \
        add_emoji_to_display_name(session, base_dir, http_prefix,
                                  nickname, domain,
                                  profile_description, False, translate)
    if profile_description:
        profile_description = standardize_text(profile_description)
    featured_hashtags = \
        get_featured_hashtags_as_html(profile_json, profile_description)
    posts_button = 'button'
    following_button = 'button'
    moved_button = 'button'
    moved_button = 'button'
    inactive_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 == 'moved':
        moved_button = 'buttonselected'
    elif selected == 'inactive':
        inactive_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)
    repo_url = get_repo_url(profile_json)
    gemini_link = get_gemini_link(profile_json)
    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)
    deltachat_invite = get_deltachat_invite(profile_json, translate)
    pronouns = get_pronouns(profile_json)
    pixelfed = get_pixelfed(profile_json)
    discord = get_discord(profile_json)
    art_site_url = get_art_site_url(profile_json)
    music_site_url = get_music_site_url(profile_json)
    youtube = get_youtube(profile_json)
    peertube = get_peertube(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)
    verified_site_checkmark = '✔'
    premium = is_premium_account(base_dir, nickname, domain)
    if donate_url or website_url or repo_url or pronouns or discord or \
       art_site_url or music_site_url or youtube or peertube or pixelfed 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 or \
       deltachat_invite:
        donate_section = '\n'
        donate_section += '  
\n'
        if donate_url and not is_system_account(nickname):
            donate_str = translate['Donate']
            if premium:
                donate_str = translate['Subscribe']
            donate_section += \
                '    ' + \
                '' + donate_str + \
                '  
\n'
        if website_url:
            if site_is_verified(session, base_dir, http_prefix,
                                nickname, domain,
                                website_url, False, debug,
                                mitm_servers):
                donate_section += \
                    '
\n'
            else:
                donate_section += \
                    '' + translate['Website'] + ': ' + \
                    '' + \
                    website_url + ' 
\n'
        if repo_url:
            donate_section += \
                '💻 ' + \
                repo_url + ' 
\n'
        if gemini_link:
            donate_section += \
                '' + 'Gemini' + ': ' + \
                gemini_link + ' 
\n'
        if email_address:
            donate_section += \
                '' + translate['Email'] + ': ' + \
                email_address + ' 
\n'
        if deltachat_invite:
            donate_section += \
                '' + translate['DeltaChat'] + ': ' + \
                deltachat_invite + ' 
\n'
        if blog_address:
            if site_is_verified(session, base_dir, http_prefix,
                                nickname, domain,
                                blog_address, False, debug,
                                mitm_servers):
                donate_section += \
                    '
\n'
            else:
                donate_section += \
                    'Blog: ' + \
                    blog_address + ' 
\n'
        if pronouns:
            donate_section += \
                '' + translate['Pronouns'] + ': ' + pronouns + '
\n'
        if xmpp_address:
            donate_section += \
                '' + translate['XMPP'] + ': ' + xmpp_address + ' 
\n'
        if pixelfed:
            donate_section += \
                'Pixelfed: ' + pixelfed + ' 
\n'
        if discord:
            donate_section += \
                'Discord: ' + discord + ' 
\n'
        if art_site_url:
            donate_section += \
                '' + translate['Art'] + ': ' + \
                art_site_url + ' 
\n'
        if music_site_url:
            donate_section += \
                '' + translate['Music'] + ': ' + \
                music_site_url + ' 
\n'
        if youtube:
            donate_section += \
                'YouTube: ' + youtube + ' 
\n'
        if peertube:
            donate_section += \
                'PeerTube: ' + peertube + ' 
\n'
        if matrix_address:
            donate_section += \
                '' + translate['Matrix'] + ': ' + matrix_address + '
\n'
        if ssb_address:
            donate_section += \
                'SSB: ' + \
                ssb_address + ' 
\n'
        if tox_address:
            donate_section += \
                'Tox: ' + \
                tox_address + ' 
\n'
        if briar_address:
            if briar_address.startswith('briar://'):
                donate_section += \
                    '' + \
                    briar_address + ' 
\n'
            else:
                donate_section += \
                    'briar://' + \
                    briar_address + ' 
\n'
        if cwtch_address:
            donate_section += \
                'Cwtch: ' + \
                cwtch_address + ' 
\n'
        if enigma_pub_key:
            donate_section += \
                'Enigma: ' + \
                enigma_pub_key + ' 
\n'
        if pgp_fingerprint:
            donate_section += \
                '' + translate['PGP Fingerprint'] + ': ' + \
                pgp_fingerprint.replace('\n', '
\n'
        if pgp_pub_key:
            donate_section += \
                '' + \
                translate['PGP Public Key'] + \
                ' ' + \
                pgp_pub_key.replace('\n', '
 \n'
        donate_section += '
' + \
            ' \n'
        logout_str = \
            '' + \
            ' \n'
        # are there any follow requests?
        follow_requests_filename = \
            acct_dir(base_dir, nickname, domain) + '/followrequests.txt'
        if os.path.isfile(follow_requests_filename):
            try:
                with open(follow_requests_filename, 'r',
                          encoding='utf-8') as fp_foll:
                    for line in fp_foll:
                        if not line:
                            continue
                        follow_approvals = True
                        followers_button = 'buttonhighlighted'
                        if selected == 'followers':
                            followers_button = 'buttonselectedhighlighted'
                        break
            except OSError as exc:
                print('EX: html_profile unable to read ' +
                      follow_requests_filename + ' ' + str(exc))
        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 fp_req:
                    for follower_handle in fp_req:
                        if not follower_handle:
                            continue
                        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 = ' ✨'
                        # Show the handle of the potential follower
                        # being approved, linking to search on that handle
                        base_path = '/users/' + nickname
                        follow_approvals_section += \
                            ''
    profile_description_short = \
        _get_profile_short_description(profile_description)
    # remove formatting from profile description used on title
    avatar_description = ''
    if profile_json.get('summary'):
        avatar_description = profile_json['summary'].replace('': '',
            '
': ''
        }
        avatar_description = replace_strings(avatar_description, replacements)
    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)
    url_str = get_url_from_post(profile_json['icon']['url'])
    avatar_url = remove_html(url_str)
    # 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/')
    account_dir = acct_dir(base_dir, nickname, domain)
    # get pinned post content
    pinned_filename = account_dir + '/pinToProfile.txt'
    pinned_content = None
    if os.path.isfile(pinned_filename):
        try:
            with open(pinned_filename, 'r', encoding='utf-8') as fp_pin:
                pinned_content = fp_pin.read()
        except OSError:
            print('EX: html_profile unable to read ' + pinned_filename)
    # shared items attached to the actor
    # https://codeberg.org/fediverse/fep/src/branch/main/fep/0837/fep-0837.md
    attached_shared_items = \
        actor_attached_shares_as_html(profile_json, max_shares_on_profile)
    birth_date = ''
    if profile_json.get('vcard:bday'):
        birth_date = profile_json['vcard:bday']
    profile_header_str = \
        _get_profile_header(base_dir, http_prefix,
                            nickname,
                            domain, domain_full, translate,
                            default_timeline, display_name,
                            pronouns, profile_description_short,
                            featured_hashtags,
                            login_button, avatar_url, theme,
                            moved_to, also_known_as,
                            pinned_content,
                            attached_shared_items,
                            access_keys, joined_date,
                            occupation_name,
                            actor_proxied, actor,
                            no_of_books, authorized,
                            birth_date, premium)
    # keyboard navigation
    user_path_str = '/users/' + nickname
    deft = default_timeline
    is_group = False
    followers_str = translate['Followers']
    if premium:
        followers_str = translate['Fans']
    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
    if show_moved_accounts:
        menu_moved = \
            html_hide_from_screen_reader('⌂') + ' ' + translate['Moved']
    menu_inactive = \
        html_hide_from_screen_reader('💤') + ' ' + translate['Inactive']
    menu_logout = \
        html_hide_from_screen_reader('❎') + ' ' + translate['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_inactive: user_path_str + '/inactive#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,
                                           None, None, None, False)
    profile_str += profile_header_str + donate_section
    profile_str += ''
    # search for following or followers
    if authorized:
        if selected in ('following', 'followers'):
            follow_search_str = '\n'
            follow_search_str += \
                '\n
\n'
            profile_str += follow_search_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 = \
        '
' + \
        ' '
    if selected == 'posts' and not premium:
        if hide_recent_posts.get(nickname):
            max_profile_posts = 0
        else:
            max_profile_posts = \
                get_max_profile_posts(base_dir, nickname, domain, 20)
        min_images_for_accounts: list[str] = []
        profile_str += \
            _html_profile_posts(recent_posts_cache, max_profile_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, {},
                                min_images_for_accounts,
                                max_profile_posts,
                                buy_sites,
                                auto_cw_cache,
                                mitm_servers,
                                instance_software) + 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,
                                        sites_unavailable,
                                        system_language,
                                        mitm_servers)
        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,
                                        sites_unavailable,
                                        system_language,
                                        mitm_servers)
    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, sites_unavailable,
                                    system_language, mitm_servers)
    if authorized and selected == 'inactive':
        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, sites_unavailable,
                                    system_language, mitm_servers)
    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 += '
\n' + \
                '     \n'
    if not following_json:
        following_json = {
            'orderedItems': []
        }
    for following_actor in following_json['orderedItems']:
        # is this a dormant followed account?
        dormant = False
        offline = False
        following_domain, _ = get_domain_from_actor(following_actor)
        if authorized:
            if following_domain in sites_unavailable:
                dormant = True
                offline = True
            else:
                if 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, offline,
                                       debug, system_language,
                                       mitm_servers,
                                       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' + \
                '     \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 += '\n
\n'
    for role in roles_list:
        if translate.get(role):
            profile_str += '
' + translate[role] + ' \n'
        else:
            profile_str += '' + role + ' \n'
    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 += \
            '\n' + \
            profile_str + '
' + profile_str + '
\n'
    return profile_str
def _grayscale_enabled(base_dir: str) -> bool:
    """Is grayscale UI enabled?
    """
    dir_str = data_dir(base_dir)
    return os.path.isfile(dir_str + '/.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 = '  ' + \
        translate['Theme'] + ' '
    for theme_name in themes:
        translated_theme_name = theme_name
        if translate.get(theme_name):
            translated_theme_name = translate[theme_name]
        themes_dropdown += '    ' + \
            translated_theme_name + ' '
    themes_dropdown += '   ',
                                ' ')
    return themes_dropdown
def _html_edit_profile_graphic_design(base_dir: str, translate: {}) -> str:
    """Graphic design section on Edit Profile screen
    """
    graphics_str = begin_edit_section(translate['Graphic Design'])
    low_bandwidth = get_config_param(base_dir, 'lowBandwidth')
    if not low_bandwidth:
        low_bandwidth = False
    graphics_str += _html_themes_dropdown(base_dir, translate)
    graphics_str += \
        '      ' + \
        translate['Import Theme'] + ' \n'
    graphics_str += '      ' + \
        translate['Export Theme'] + ' ➤ ' + \
        translate['Instance Logo'] + ' ' + \
        '  \n' + \
        translate['Security'] + ' ' + \
        translate['Type of instance'] + ' '
    # site moderators
    moderators = ''
    moderators_file = data_dir(base_dir) + '/moderators.txt'
    if os.path.isfile(moderators_file):
        try:
            with open(moderators_file, 'r', encoding='utf-8') as fp_mod:
                moderators = fp_mod.read()
        except OSError:
            print('EX: _html_edit_profile_instance unable to read ' +
                  moderators_file)
    subtitle = translate['A list of moderator nicknames. One per line.']
    role_assign_str += \
        edit_text_area('' + translate['Moderators'] + ' ', subtitle,
                       'moderators', moderators, 200, '', False)
    # site editors
    editors = ''
    editors_file = data_dir(base_dir) + '/editors.txt'
    if os.path.isfile(editors_file):
        try:
            with open(editors_file, 'r', encoding='utf-8') as fp_edit:
                editors = fp_edit.read()
        except OSError:
            print('EX: _html_edit_profile_instance unable to read ' +
                  editors_file)
    subtitle = translate['A list of editor nicknames. One per line.']
    role_assign_str += \
        edit_text_area('' + translate['Site Editors'] + ' ',
                       subtitle, 'editors', editors, 200, '', False)
    # counselors
    counselors = ''
    counselors_file = data_dir(base_dir) + '/counselors.txt'
    if os.path.isfile(counselors_file):
        try:
            with open(counselors_file, 'r', encoding='utf-8') as fp_co:
                counselors = fp_co.read()
        except OSError:
            print('EX: _html_edit_profile_instance unable to read ' +
                  counselors_file)
    role_assign_str += \
        edit_text_area('' + translate['Counselors'] + ' ', None,
                       'counselors', counselors, 200, '', False)
    # artists
    artists = ''
    artists_file = data_dir(base_dir) + '/artists.txt'
    if os.path.isfile(artists_file):
        try:
            with open(artists_file, 'r', encoding='utf-8') as fp_art:
                artists = fp_art.read()
        except OSError:
            print('EX: _html_edit_profile_instance unable to read ' +
                  artists_file)
    role_assign_str += \
        edit_text_area('' + translate['Artists'] + ' ', None,
                       'artists', artists, 200, '', False)
    # site devops
    devops = ''
    devops_file = data_dir(base_dir) + '/devops.txt'
    if os.path.isfile(devops_file):
        try:
            with open(devops_file, 'r', encoding='utf-8') as fp_edit:
                devops = fp_edit.read()
        except OSError:
            print('EX: _html_edit_profile_instance unable to read ' +
                  devops_file)
    subtitle = translate['A list of devops nicknames. One per line.']
    role_assign_str += \
        edit_text_area('' + translate['Site DevOps'] + ' ',
                       subtitle, 'devopslist', devops, 200, '', False)
    role_assign_str += end_edit_section()
    # Video section
    peertube_str = begin_edit_section(translate['Video Settings'])
    peertube_instances_str = ''
    for url in peertube_instances:
        peertube_instances_str += url + '\n'
    peertube_str += \
        edit_text_area(translate['Peertube Instances'], None,
                       'ptInstances', peertube_instances_str, 200, '', False)
    peertube_str += \
        '      ' + \
        translate['Danger Zone'] + ' 📊 GET 
'
    system_monitor_str += '📊 INBOX 
'
    system_monitor_str += '📊 POST 
'
    system_monitor_str += end_edit_section()
    return system_monitor_str
def _html_edit_profile_skills(base_dir: str, nickname: str, domain: str,
                              translate: {}) -> str:
    """skills section of Edit Profile screen
    """
    system_language = 'en'
    skills = get_skills(base_dir, nickname, domain)
    skills_str = ''
    skill_ctr = 1
    if skills:
        for skill_desc, skill_value in skills.items():
            if is_filtered(base_dir, nickname, domain, skill_desc,
                           system_language):
                continue
            skills_str += \
                '
'
            skill_ctr += 1
    skills_str += \
        '
' + end_edit_section()
    idx = 'If you want to participate within organizations then you ' + \
        'can indicate some skills that you have and approximate ' + \
        'proficiency levels. This helps organizers to construct ' + \
        'teams with an appropriate combination of skills.'
    edit_profile_form = \
        begin_edit_section(translate['Skills']) + \
        '      ' + \
        translate['Skills'] + ' ' + \
        translate[idx] + ' \n' + skills_str
    return edit_profile_form
def _html_edit_profile_git_projects(base_dir: str, nickname: str, domain: str,
                                    translate: {}) -> str:
    """git projects section of edit profile screen
    """
    git_projects_str = ''
    git_projects_filename = \
        acct_dir(base_dir, nickname, domain) + '/gitprojects.txt'
    if os.path.isfile(git_projects_filename):
        try:
            with open(git_projects_filename, 'r',
                      encoding='utf-8') as fp_git:
                git_projects_str = fp_git.read()
        except OSError:
            print('EX: _html_edit_profile_git_projects unable to read ' +
                  git_projects_filename)
    edit_profile_form = begin_edit_section(translate['Git Projects'])
    idx = 'List of project names that you wish to receive git patches for'
    edit_profile_form += \
        edit_text_area(translate[idx], None, 'gitProjects', git_projects_str,
                       100, '', False)
    edit_profile_form += end_edit_section()
    return edit_profile_form
def _html_edit_profile_shared_items(base_dir: str, translate: {}) -> str:
    """shared items section of edit profile screen
    """
    shared_items_str = ''
    shared_items_federated_domains_str = \
        get_config_param(base_dir, 'sharedItemsFederatedDomains')
    if shared_items_federated_domains_str:
        shared_items_federated_domains_list = \
            shared_items_federated_domains_str.split(',')
        for shared_federated_domain in shared_items_federated_domains_list:
            shared_items_str += shared_federated_domain.strip() + '\n'
    edit_profile_form = begin_edit_section(translate['Shares'])
    idx = 'List of domains which can access the shared items catalog'
    edit_profile_form += \
        edit_text_area(translate[idx], None, 'shareDomainList',
                       shared_items_str, 200, '', False)
    edit_profile_form += end_edit_section()
    return edit_profile_form
def _html_edit_profile_filtering(base_dir: str, nickname: str, domain: str,
                                 user_agents_blocked: str,
                                 crawlers_allowed: str,
                                 translate: {}, reply_interval_hours: int,
                                 cw_lists: {}, lists_enabled: str,
                                 buy_sites: {}, block_military: {},
                                 block_government: {},
                                 block_bluesky: {},
                                 block_nostr: {},
                                 block_federated_endpoints: []) -> str:
    """Filtering and blocking section of edit profile screen
    """
    filter_str = ''
    filter_filename = \
        acct_dir(base_dir, nickname, domain) + '/filters.txt'
    if os.path.isfile(filter_filename):
        try:
            with open(filter_filename, 'r', encoding='utf-8') as fp_filter:
                filter_str = fp_filter.read()
        except OSError:
            print('EX: _html_edit_profile_filtering unable to read ' +
                  filter_filename)
    filter_bio_str = ''
    filter_bio_filename = \
        acct_dir(base_dir, nickname, domain) + '/filters_bio.txt'
    if os.path.isfile(filter_bio_filename):
        try:
            with open(filter_bio_filename, 'r',
                      encoding='utf-8') as fp_filter:
                filter_bio_str = fp_filter.read()
        except OSError:
            print('EX: _html_edit_profile_filtering unable to read ' +
                  filter_bio_filename)
    switch_str = ''
    switch_filename = \
        acct_dir(base_dir, nickname, domain) + '/replacewords.txt'
    if os.path.isfile(switch_filename):
        try:
            with open(switch_filename, 'r', encoding='utf-8') as fp_switch:
                switch_str = fp_switch.read()
        except OSError:
            print('EX: _html_edit_profile_filtering unable to save ' +
                  switch_filename)
    auto_tags = ''
    auto_tags_filename = \
        acct_dir(base_dir, nickname, domain) + '/autotags.txt'
    if os.path.isfile(auto_tags_filename):
        try:
            with open(auto_tags_filename, 'r', encoding='utf-8') as fp_auto:
                auto_tags = fp_auto.read()
        except OSError:
            print('EX: _html_edit_profile_filtering unable to read ' +
                  auto_tags_filename)
    auto_cw = ''
    auto_cw_filename = \
        acct_dir(base_dir, nickname, domain) + '/autocw.txt'
    if os.path.isfile(auto_cw_filename):
        try:
            with open(auto_cw_filename, 'r', encoding='utf-8') as fp_cw:
                auto_cw = fp_cw.read()
        except OSError:
            print('EX: _html_edit_profile_filtering unable to read ' +
                  auto_cw_filename)
    blocked_str = get_account_blocks(base_dir, nickname, domain)
    dm_allowed_instances_str = ''
    dm_allowed_instances_filename = \
        acct_dir(base_dir, nickname, domain) + '/dmAllowedInstances.txt'
    if os.path.isfile(dm_allowed_instances_filename):
        try:
            with open(dm_allowed_instances_filename, 'r',
                      encoding='utf-8') as fp_dm:
                dm_allowed_instances_str = fp_dm.read()
        except OSError:
            print('EX: _html_edit_profile_filtering unable to read ' +
                  dm_allowed_instances_filename)
    allowed_instances_str = ''
    allowed_instances_filename = \
        acct_dir(base_dir, nickname, domain) + '/allowedinstances.txt'
    if os.path.isfile(allowed_instances_filename):
        try:
            with open(allowed_instances_filename, 'r',
                      encoding='utf-8') as fp_allow:
                allowed_instances_str = fp_allow.read()
        except OSError:
            print('EX: _html_edit_profile_filtering unable to read ' +
                  allowed_instances_filename)
    edit_profile_form = begin_edit_section(translate['Filtering and Blocking'])
    idx = 'Hours after posting during which replies are allowed'
    edit_profile_form += \
        '  ' + \
        translate[idx] + \
        ':  ' + \
        translate['City for spoofed GPS image metadata'] + \
        ' \n'
    city = city.lower()
    for city_name in cities:
        if ':' not in city_name:
            continue
        city_selected = ''
        city_name = city_name.split(':')[0]
        city_name = city_name.lower()
        if city:
            if city in city_name:
                city_selected = ' selected'
        edit_profile_form += \
            '    ' + \
            city_name + ' \n'
    edit_profile_form += '   ' + \
        translate['Filtered words'] + ' ' + \
        translate['One per line'] + ' \n' + \
        '      \n' + \
        '      ' + \
        translate['Filtered words within bio'] + ' ' + \
        translate['One per line'] + ' \n' + \
        '      \n' + \
        '      ' + \
        translate['Word Replacements'] + ' A -> B \n' + \
        '      \n' + \
        '      ' + \
        translate['Autogenerated Hashtags'] + ' A -> #B \n' + \
        '      \n' + \
        '      ' + \
        translate['Autogenerated Content Warnings'] + ' A -> B \n' + \
        '      \n'
    idx = 'Blocked accounts, one per line, in the form ' + \
        'nickname@domain or *@blockeddomain'
    edit_profile_form += \
        edit_text_area(translate['Blocked accounts'], None, 'blocked',
                       blocked_str, 500, '', False)
    # import and export blocks
    edit_profile_form += _import_export_blocks(translate)
    idx = 'Direct messages are always allowed from these instances.'
    edit_profile_form += \
        edit_text_area(translate['Direct Message permitted instances'], None,
                       'dmAllowedInstances', dm_allowed_instances_str,
                       200, '', False)
    idx = 'Federate only with a defined set of instances. ' + \
        'One domain name per line.'
    edit_profile_form += \
        '      ' + \
        translate['Federation list'] + ' ' + \
        translate[idx] + ' \n' + \
        '      \n'
    if is_moderator(base_dir, nickname):
        edit_profile_form += \
            '' + \
            translate['Known Web Crawlers'] + ' ' + \
            translate['Known Search Bots'] + ' ' + translate[idx] + ': \n' + \
                '' + translate['Change Password'] + \
        ' ' + translate['Confirm Password'] + \
        ' ' + translate[idx] + ' ' + \
            translate['Background image'] + ' \n' + \
            '      ' + \
            translate['Timeline banner image'] + ' \n' + \
            '      ' + \
            translate['Search banner image'] + ' \n' + \
            '      ' + \
            translate['Left column image'] + ' \n' + \
            '      ' + \
            translate['Right column image'] + ' \n' + \
            '      ' + \
            translate['Watermark image'] + ' \n' + \
            '      ' + \
        translate['Import Follows'] + ' \n'
    edit_profile_form += '
\n'
    edit_profile_form += \
        '' + \
        translate['Following'] + ' ' + \
        '⇩ CSV  
' + \
        followers_str + ' 
\n'
    edit_profile_form += _import_export_blocks(translate)
    edit_profile_form += end_edit_section()
    return edit_profile_form
def _html_edit_profile_encryption_keys(pgp_fingerprint: str,
                                       pgp_pub_key: str,
                                       enigma_pub_key: str,
                                       translate: {}) -> str:
    """Contact Information section of edit profile screen
    """
    edit_profile_form = begin_edit_section(translate['Encryption Keys'])
    enigma_url = 'https://github.com/enigma-reloaded/enigma-reloaded'
    edit_profile_form += \
        edit_text_field('Enigma ',
                        'enigmapubkey', enigma_pub_key)
    edit_profile_form += edit_text_field(translate['PGP Fingerprint'],
                                         'openpgp', pgp_fingerprint)
    edit_profile_form += \
        edit_text_area(translate['PGP'], None, 'pgp', pgp_pub_key, 600,
                       '-----BEGIN PGP PUBLIC KEY BLOCK-----', False)
    edit_profile_form += end_edit_section()
    return edit_profile_form
def _html_edit_profile_reply_controls(translate: {},
                                      follow_dms: bool,
                                      premium: bool,
                                      show_replies_followers: bool,
                                      show_replies_mutuals: bool,
                                      no_reply_boosts: bool) -> str:
    """option checkboxes for reply controls
    """
    edit_profile_form = begin_edit_section(translate['Reply Controls'])
    edit_profile_form += \
        edit_check_box(translate['Only people I follow can send me DMs'],
                       'followDMs', follow_dms)
    show_replies_followers_str = translate['Only allow replies from followers']
    if premium:
        show_replies_followers_str = translate['Only allow replies from fans']
    edit_profile_form += \
        edit_check_box(show_replies_followers_str, 'repliesFromFollowersOnly',
                       show_replies_followers)
    show_replies_mutuals_str = translate['Only allow replies from mutuals']
    edit_profile_form += \
        edit_check_box(show_replies_mutuals_str, 'repliesFromMutualsOnly',
                       show_replies_mutuals)
    no_reply_boosts_str = translate["Don't show boosted replies"]
    edit_profile_form += \
        edit_check_box(no_reply_boosts_str, 'noReplyBoosts', no_reply_boosts)
    edit_profile_form += end_edit_section()
    return edit_profile_form
def _html_edit_profile_options(is_admin: bool,
                               manually_approves_followers: str,
                               reject_spam_actors: str,
                               is_bot: str, is_group: str,
                               remove_twitter: str,
                               notify_likes: str, notify_reactions: str,
                               hide_like_button: str,
                               hide_reaction_button: str,
                               translate: {}, bold_reading: bool,
                               nickname: str,
                               min_images_for_accounts: [],
                               reverse_sequence: [],
                               show_quote_toots: bool,
                               show_vote_posts: bool,
                               hide_follows: bool,
                               hide_recent_posts: bool,
                               premium: bool,
                               no_seen_posts: bool,
                               watermark_enabled: bool) -> str:
    """option checkboxes section of edit profile screen
    """
    edit_profile_form = '    \n'
    edit_profile_form += \
        edit_check_box(translate['Premium account'], 'premiumAccount', premium)
    approve_followers_str = translate['Approve follower requests']
    if premium:
        approve_followers_str = translate['Approve fans']
    edit_profile_form += \
        edit_check_box(approve_followers_str,
                       'approveFollowers', manually_approves_followers)
    edit_profile_form += \
        edit_check_box(translate['Reject spam accounts'],
                       'rejectSpamActors', reject_spam_actors)
    edit_profile_form += \
        edit_check_box(translate['This is a bot account'],
                       'isBot', is_bot)
    if is_admin:
        edit_profile_form += \
            edit_check_box(translate['This is a group account'],
                           'isGroup', is_group)
    edit_profile_form += \
        edit_check_box(translate['Remove Twitter posts'],
                       'removeTwitter', remove_twitter)
    edit_profile_form += \
        edit_check_box(translate['Notify when posts are liked'],
                       'notifyLikes', notify_likes)
    edit_profile_form += \
        edit_check_box(translate['Notify on emoji reactions'],
                       'notifyReactions', notify_reactions)
    edit_profile_form += \
        edit_check_box(translate["Don't show the Like button"],
                       'hideLikeButton', hide_like_button)
    edit_profile_form += \
        edit_check_box(translate["Don't show the Reaction button"],
                       'hideReactionButton', hide_reaction_button)
    bold_str = bold_reading_string(translate['Bold reading'])
    edit_profile_form += \
        edit_check_box(bold_str, 'boldReading', bold_reading)
    minimize_all_images = False
    if nickname in min_images_for_accounts:
        minimize_all_images = True
    minimize_all_images_str = translate['Minimize all images']
    edit_profile_form += \
        edit_check_box(minimize_all_images_str, 'minimizeAllImages',
                       minimize_all_images)
    reverse = False
    if nickname in reverse_sequence:
        reverse = True
    reverse_str = translate['Reverse timelines']
    edit_profile_form += \
        edit_check_box(reverse_str, 'reverseTimelines', reverse)
    show_quote_toots_str = translate['Show quote posts']
    edit_profile_form += \
        edit_check_box(show_quote_toots_str, 'showQuotes', show_quote_toots)
    show_vote_posts_str = translate['Show vote posts']
    edit_profile_form += \
        edit_check_box(show_vote_posts_str, 'showVotes', show_vote_posts)
    hide_follows_str = translate['Do not show follows on your profile']
    if premium:
        hide_follows_str = translate['Do not show fans on your profile']
    edit_profile_form += \
        edit_check_box(hide_follows_str, 'hideFollows', hide_follows)
    hide_recent_posts_str = \
        translate["Don't show recent public posts on your profile"]
    edit_profile_form += \
        edit_check_box(hide_recent_posts_str, 'hideRecentPosts',
                       hide_recent_posts)
    no_seen_posts_str = translate["Don't show already seen posts"]
    edit_profile_form += \
        edit_check_box(no_seen_posts_str, 'noSeenPosts', no_seen_posts)
    watermark_str = translate["Apply a watermark to uploaded images"]
    edit_profile_form += \
        edit_check_box(watermark_str, 'watermarkEnabled', watermark_enabled)
    edit_profile_form += '    
\n'
    return edit_profile_form
def _get_supported_languagesSorted(base_dir: str) -> str:
    """Returns a list of supported languages
    """
    lang_list = get_supported_languages(base_dir)
    if not lang_list:
        return ''
    lang_list.sort()
    languages_str = ''
    for lang in lang_list:
        if languages_str:
            languages_str += ' / ' + lang
        else:
            languages_str = lang
    return languages_str
def _html_edit_profile_main(base_dir: str, display_nickname: str,
                            pronouns: str, bio_str: str,
                            moved_to: str, donate_url: str, website_url: str,
                            gemini_link: str, blog_address: str,
                            actor_json: {}, translate: {},
                            nickname: str, domain: str,
                            max_recent_posts: int,
                            featured_hashtags: str) -> str:
    """main info on edit profile screen
    """
    image_formats = get_image_formats()
    edit_profile_form = '    \n'
    edit_profile_form += \
        edit_text_field(translate['Nickname'], 'displayNickname',
                        display_nickname)
    edit_profile_form += \
        edit_text_field(translate['Pronouns'], 'setPronouns',
                        pronouns)
    edit_profile_form += \
        edit_text_area(translate['Your bio'], None, 'bio', bio_str,
                       200, '', True)
    birth_date = ''
    birth_date_field = 'vcard:bday'
    if actor_json.get(birth_date_field):
        if '-' in actor_json[birth_date_field]:
            if len(actor_json[birth_date_field].split('-')) == 3:
                if 'T' in actor_json[birth_date_field]:
                    actor_json[birth_date_field] = \
                        actor_json[birth_date_field].split('T')[0]
                birth_date = actor_json[birth_date_field]
    edit_profile_form += '' + \
        translate['Birthday'] + ':  \n'
    edit_profile_form += \
        '' + translate['Avatar image'] + \
        ' \n' + \
        '      
\n'
    return edit_profile_form
def _html_edit_profile_top_banner(base_dir: str,
                                  nickname: str, domain: str, domain_full: str,
                                  default_timeline: str, banner_file: str,
                                  path: str, access_keys: {},
                                  translate: {}) -> str:
    """top banner on edit profile screen
    """
    edit_profile_form = \
        '' + \
        ' \n'
    edit_profile_form += \
        '\n'
    edit_profile_form += html_footer()
    return edit_profile_form
def _individual_follow_as_html(signing_priv_key_pem: str,
                               translate: {},
                               base_dir: str, session,
                               cached_webfingers: {},
                               person_cache: {}, domain: str,
                               follow_url: str,
                               authorized: bool,
                               actor_nickname: str,
                               http_prefix: str,
                               project_version: str,
                               dormant: bool,
                               offline: bool,
                               debug: bool,
                               system_language: str,
                               mitm_servers: [],
                               buttons: list[str] = []) -> str:
    """An individual follow entry on the profile screen
    """
    follow_url_nickname = get_nickname_from_actor(follow_url)
    if not follow_url_nickname:
        return ''
    follow_url_domain, follow_url_port = get_domain_from_actor(follow_url)
    if not follow_url_domain:
        return ''
    follow_url_domain_full = \
        get_full_domain(follow_url_domain, follow_url_port)
    title_str = '@' + follow_url_nickname + '@' + follow_url_domain_full
    avatar_url = \
        get_person_avatar_url(base_dir, follow_url, person_cache)
    if not avatar_url:
        avatar_url = follow_url + '/avatar.png'
    display_name = get_display_name(base_dir, follow_url, person_cache)
    is_group = False
    if not display_name:
        # lookup the correct webfinger for the follow_url
        follow_url_handle = follow_url_nickname + '@' + follow_url_domain_full
        follow_url_wf = \
            webfinger_handle(session, follow_url_handle, http_prefix,
                             cached_webfingers,
                             domain, __version__, debug, False,
                             signing_priv_key_pem, mitm_servers)
        origin_domain = domain
        (_, _, _, _, _, avatar_url2,
         display_name, is_group) = get_person_box(signing_priv_key_pem,
                                                  origin_domain,
                                                  base_dir, session,
                                                  follow_url_wf,
                                                  person_cache,
                                                  project_version,
                                                  http_prefix,
                                                  follow_url_nickname,
                                                  domain, 'outbox', 43036,
                                                  system_language,
                                                  mitm_servers)
        if avatar_url2:
            avatar_url = avatar_url2
    if display_name:
        display_name = \
            add_emoji_to_display_name(None, base_dir, http_prefix,
                                      actor_nickname, domain,
                                      display_name, False, translate)
        title_str = display_name
    if offline:
        title_str += ' [' + translate['offline'].upper() + '] '
    elif dormant:
        title_str += ' 💤'
    buttons_str = ''
    if authorized:
        for btn in buttons:
            if btn == 'block':
                buttons_str += \
                    '' + \
                    translate['Block'] + ' ' + \
                    translate[unfollow_str] + ' ' + \
                    translate['Move'] + '