__filename__ = "webapp_post.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
import time
import urllib.parse
from dateutil.parser import parse
from auth import create_password
from git import is_git_patch
from datetime import datetime
from cache import get_person_from_cache
from bookmarks import bookmarked_by_person
from announce import announced_by_person
from announce import no_of_announces
from like import liked_by_person
from like import no_of_likes
from follow import is_following_actor
from posts import post_is_muted
from posts import get_person_box
from posts import download_announce
from posts import populate_replies_json
from utils import remove_eol
from utils import disallow_announce
from utils import disallow_reply
from utils import convert_published_to_local_timezone
from utils import remove_hash_from_post_id
from utils import remove_html
from utils import get_actor_languages_list
from utils import get_base_content_from_post
from utils import get_content_from_post
from utils import get_summary_from_post
from utils import has_object_dict
from utils import update_announce_collection
from utils import is_pgp_encrypted
from utils import is_dm
from utils import is_chat_message
from utils import reject_post_id
from utils import is_recent_post
from utils import get_config_param
from utils import get_full_domain
from utils import is_editor
from utils import locate_post
from utils import load_json
from utils import get_cached_post_directory
from utils import get_cached_post_filename
from utils import get_protocol_prefixes
from utils import is_news_post
from utils import is_blog_post
from utils import get_display_name
from utils import display_name_is_emoji
from utils import is_public_post
from utils import update_recent_posts_cache
from utils import remove_id_ending
from utils import get_nickname_from_actor
from utils import get_domain_from_actor
from utils import acct_dir
from utils import local_actor_url
from utils import is_unlisted_post
from content import create_edits_html
from content import bold_reading_string
from content import limit_repeated_words
from content import replace_emoji_from_tags
from content import html_replace_quote_marks
from content import html_replace_email_quote
from content import remove_text_formatting
from content import remove_long_words
from content import get_mentions_from_html
from content import switch_words
from person import is_person_snoozed
from person import get_person_avatar_url
from webapp_utils import get_banner_file
from webapp_utils import get_avatar_image_url
from webapp_utils import update_avatar_image_cache
from webapp_utils import load_individual_post_as_html_from_cache
from webapp_utils import add_emoji_to_display_name
from webapp_utils import post_contains_public
from webapp_utils import get_content_warning_button
from webapp_utils import get_post_attachments_as_html
from webapp_utils import html_header_with_external_style
from webapp_utils import html_footer
from webapp_utils import get_broken_link_substitute
from webapp_media import add_embedded_elements
from webapp_question import insert_question
from devices import e2e_edecrypt_message_from_device
from webfinger import webfinger_handle
from speaker import update_speaker
from languages import auto_translate_post
from blocking import is_blocked
from blocking import add_cw_from_lists
from reaction import html_emoji_reactions
from maps import html_open_street_map
from maps import set_map_preferences_coords
from maps import set_map_preferences_url
from maps import geocoords_from_map_link
def _get_location_from_tags(tags: []) -> str:
    """Returns the location from the tags list
    """
    for tag_item in tags:
        if not tag_item.get('type'):
            continue
        if tag_item['type'] != 'Place':
            continue
        if not tag_item.get('name'):
            continue
        if not isinstance(tag_item['name'], str):
            continue
        return tag_item['name'].replace('\n', ' ')
    return None
def _html_post_metadata_open_graph(domain: str, post_json_object: {},
                                   system_language: str) -> str:
    """Returns html OpenGraph metadata for a post
    """
    metadata = \
        "    \n"
    metadata += \
        "    \n"
    obj_json = post_json_object
    if has_object_dict(post_json_object):
        obj_json = post_json_object['object']
    if obj_json.get('attributedTo'):
        if isinstance(obj_json['attributedTo'], str):
            attrib = obj_json['attributedTo']
            actor_nick = get_nickname_from_actor(attrib)
            if actor_nick:
                actor_domain, _ = get_domain_from_actor(attrib)
                actor_handle = actor_nick + '@' + actor_domain
                metadata += \
                    "    \n"
    if obj_json.get('url'):
        metadata += \
            "    \n"
    if obj_json.get('published'):
        metadata += \
            "    \n"
    if not obj_json.get('attachment') or obj_json.get('sensitive'):
        if obj_json.get('content') and not obj_json.get('sensitive'):
            obj_content = obj_json['content']
            if obj_json.get('contentMap'):
                if obj_json['contentMap'].get(system_language):
                    obj_content = obj_json['contentMap'][system_language]
            description = remove_html(obj_content)
            metadata += \
                "    \n"
            metadata += \
                "    \n"
        return metadata
    # metadata for attachment
    for attach_json in obj_json['attachment']:
        if not isinstance(attach_json, dict):
            continue
        if not attach_json.get('mediaType'):
            continue
        if not attach_json.get('url'):
            continue
        if not attach_json.get('name'):
            continue
        description = None
        if attach_json['mediaType'].startswith('image/'):
            description = 'Attached: 1 image'
        elif attach_json['mediaType'].startswith('video/'):
            description = 'Attached: 1 video'
        elif attach_json['mediaType'].startswith('audio/'):
            description = 'Attached: 1 audio'
        if description:
            if obj_json.get('content') and not obj_json.get('sensitive'):
                obj_content = obj_json['content']
                if obj_json.get('contentMap'):
                    if obj_json['contentMap'].get(system_language):
                        obj_content = obj_json['contentMap'][system_language]
                description += '\n\n' + remove_html(obj_content)
            metadata += \
                "    \n"
            metadata += \
                "    \n"
            metadata += \
                "    \n"
            metadata += \
                "    \n"
            if attach_json.get('width'):
                metadata += \
                    "    \n"
            if attach_json.get('height'):
                metadata += \
                    "    \n"
            metadata += \
                "    \n"
            if attach_json['mediaType'].startswith('image/'):
                metadata += \
                    "    \n"
    return metadata
def _log_post_timing(enable_timing_log: bool, post_start_time,
                     debug_id: str) -> None:
    """Create a log of timings for performance tuning
    """
    if not enable_timing_log:
        return
    time_diff = int((time.time() - post_start_time) * 1000)
    if time_diff > 100:
        print('TIMING INDIV ' + debug_id + ' = ' + str(time_diff))
def prepare_html_post_nickname(nickname: str, post_html: str) -> str:
    """html posts stored in memory are for all accounts on the instance
    and they're indexed by id. However, some incoming posts may be
    destined for multiple accounts (followers). This creates a problem
    where the icon links whose urls begin with href="/users/nickname?
    need to be changed for different nicknames to display correctly
    within their timelines.
    This function changes the nicknames for the icon links.
    """
    # replace the nickname
    users_str = ' href="/users/'
    if users_str not in post_html:
        return post_html
    user_found = True
    post_str = post_html
    new_post_str = ''
    while user_found:
        if users_str not in post_str:
            new_post_str += post_str
            break
        # the next part, after href="/users/nickname?
        next_str = post_str.split(users_str, 1)[1]
        if '?' in next_str:
            next_str = next_str.split('?', 1)[1]
        else:
            new_post_str += post_str
            break
        # append the previous text to the result
        new_post_str += post_str.split(users_str)[0]
        new_post_str += users_str + nickname + '?'
        # post is now the next part
        post_str = next_str
    return new_post_str
def prepare_post_from_html_cache(nickname: str, post_html: str, box_name: str,
                                 page_number: int) -> str:
    """Sets the page number on a cached html post
    """
    # if on the bookmarks timeline then remain there
    if box_name in ('tlbookmarks', 'bookmarks'):
        post_html = post_html.replace('?tl=inbox', '?tl=tlbookmarks')
        if '?page=' in post_html:
            page_number_str = post_html.split('?page=')[1]
            if '?' in page_number_str:
                page_number_str = page_number_str.split('?')[0]
            post_html = \
                post_html.replace('?page=' + page_number_str, '?page=-999')
    with_page_number = \
        post_html.replace(';-999;', ';' + str(page_number) + ';')
    with_page_number = \
        with_page_number.replace('?page=-999', '?page=' + str(page_number))
    return prepare_html_post_nickname(nickname, with_page_number)
def _save_individual_post_as_html_to_cache(base_dir: str,
                                           nickname: str, domain: str,
                                           post_json_object: {},
                                           post_html: str) -> bool:
    """Saves the given html for a post to a cache file
    This is so that it can be quickly reloaded on subsequent
    refresh of the timeline
    """
    html_post_cache_dir = \
        get_cached_post_directory(base_dir, nickname, domain)
    cached_post_filename = \
        get_cached_post_filename(base_dir, nickname, domain, post_json_object)
    # create the cache directory if needed
    if not os.path.isdir(html_post_cache_dir):
        os.mkdir(html_post_cache_dir)
    try:
        with open(cached_post_filename, 'w+', encoding='utf-8') as fp_cache:
            fp_cache.write(post_html)
            return True
    except Exception as ex:
        print('ERROR: saving post to cache, ' + str(ex))
    return False
def _get_post_from_recent_cache(session,
                                base_dir: str,
                                http_prefix: str,
                                nickname: str, domain: str,
                                post_json_object: {},
                                post_actor: str,
                                person_cache: {},
                                allow_downloads: bool,
                                show_public_only: bool,
                                store_to_cache: bool,
                                box_name: str,
                                avatar_url: str,
                                enable_timing_log: bool,
                                post_start_time,
                                page_number: int,
                                recent_posts_cache: {},
                                max_recent_posts: int,
                                signing_priv_key_pem: str) -> str:
    """Attempts to get the html post from the recent posts cache in memory
    """
    if box_name == 'tlmedia':
        return None
    if show_public_only:
        return None
    try_cache = False
    bm_timeline = box_name in ('bookmarks', 'tlbookmarks')
    if store_to_cache or bm_timeline:
        try_cache = True
    if not try_cache:
        return None
    # update avatar if needed
    if not avatar_url:
        avatar_url = \
            get_person_avatar_url(base_dir, post_actor, person_cache)
        _log_post_timing(enable_timing_log, post_start_time, '2.1')
    update_avatar_image_cache(signing_priv_key_pem,
                              session, base_dir, http_prefix,
                              post_actor, avatar_url, person_cache,
                              allow_downloads)
    _log_post_timing(enable_timing_log, post_start_time, '2.2')
    post_html = \
        load_individual_post_as_html_from_cache(base_dir, nickname, domain,
                                                post_json_object)
    if not post_html:
        return None
    post_html = \
        prepare_post_from_html_cache(nickname, post_html,
                                     box_name, page_number)
    update_recent_posts_cache(recent_posts_cache, max_recent_posts,
                              post_json_object, post_html)
    _log_post_timing(enable_timing_log, post_start_time, '3')
    return post_html
def _get_avatar_image_html(show_avatar_options: bool,
                           nickname: str, domain_full: str,
                           avatar_url: str, post_actor: str,
                           translate: {}, avatar_position: str,
                           page_number: int, message_id_str: str) -> str:
    """Get html for the avatar image
    """
    # don't use svg images
    if avatar_url.endswith('.svg'):
        avatar_url = '/icons/avatar_default.png'
    avatar_link = ''
    if '/users/news/' not in avatar_url:
        avatar_link = \
            '        '
        show_profile_str = 'Show profile'
        if translate.get(show_profile_str):
            show_profile_str = translate[show_profile_str]
        avatar_link += \
            ' ' + translated_citations_str + ': ' + by_text + ' @' + \
            by_str_handle + '' + by_text_extra + '\n'
        domain_full = get_full_domain(domain, port)
        actor = '/users/' + nickname
        follow_str = '  \n'
    if show_avatar_options and \
       domain_full + '/users/' + nickname not in post_actor:
        show_options_for_this_person_str = 'Show options for this person'
        if translate.get(show_options_for_this_person_str):
            show_options_for_this_person_str = \
                translate[show_options_for_this_person_str]
        if '/users/news/' not in avatar_url:
            avatar_link = \
                '        \n'
            avatar_link += \
                '        
\n'
        else:
            # don't link to the person options for the news account
            avatar_link += \
                '        
\n'
    return avatar_link.strip()
def _get_reply_icon_html(base_dir: str, nickname: str, domain: str,
                         is_public_reply: bool, is_unlisted_reply: bool,
                         show_icons: bool, comments_enabled: bool,
                         post_json_object: {}, page_number_param: str,
                         translate: {}, system_language: str,
                         conversation_id: str) -> str:
    """Returns html for the reply icon/button
    """
    reply_str = ''
    if not (show_icons and comments_enabled):
        return reply_str
    # reply is permitted - create reply icon
    reply_to_link = remove_hash_from_post_id(post_json_object['object']['id'])
    reply_to_link = remove_id_ending(reply_to_link)
    # see Mike MacGirvin's replyTo suggestion
    if post_json_object['object'].get('replyTo'):
        # check that the alternative replyTo url is not blocked
        block_nickname = \
            get_nickname_from_actor(post_json_object['object']['replyTo'])
        if not block_nickname:
            return reply_str
        block_domain, _ = \
            get_domain_from_actor(post_json_object['object']['replyTo'])
        if not is_blocked(base_dir, nickname, domain,
                          block_nickname, block_domain, {}):
            reply_to_link = post_json_object['object']['replyTo']
    if post_json_object['object'].get('attributedTo'):
        if isinstance(post_json_object['object']['attributedTo'], str):
            reply_to_link += \
                '?mention=' + post_json_object['object']['attributedTo']
    content = get_base_content_from_post(post_json_object, system_language)
    if content:
        mentioned_actors = \
            get_mentions_from_html(content,
                                   " 500:
                        break
    reply_to_link += page_number_param
    reply_str = ''
    reply_to_this_post_str = 'Reply to this post'
    if translate.get(reply_to_this_post_str):
        reply_to_this_post_str = translate[reply_to_this_post_str]
    conversation_str = ''
    if conversation_id:
        conversation_str = '?conversationId=' + conversation_id
    if is_public_reply:
        reply_str += \
            '        \n'
    elif is_unlisted_reply:
        reply_str += \
            '        \n'
    else:
        if is_dm(post_json_object):
            reply_type = 'replydm'
            if is_chat_message(post_json_object):
                reply_type = 'replychat'
            reply_str += \
                '        ' + \
                '\n'
        else:
            reply_str += \
                '        ' + \
                '\n'
    reply_str += \
        '        ' + \
        '
 \n'
    return reply_str
def _get_edit_icon_html(base_dir: str, nickname: str, domain_full: str,
                        post_json_object: {}, actor_nickname: str,
                        translate: {}, is_event: bool) -> str:
    """Returns html for the edit icon/button
    """
    edit_str = ''
    actor = post_json_object['actor']
    # This should either be a post which you created,
    # or it could be generated from the newswire (see
    # _add_blogs_to_newswire) in which case anyone with
    # editor status should be able to alter it
    if (actor.endswith('/' + domain_full + '/users/' + nickname) or
        (is_editor(base_dir, nickname) and
         actor.endswith('/' + domain_full + '/users/news'))):
        post_id = remove_id_ending(post_json_object['object']['id'])
        if '/statuses/' not in post_id:
            return edit_str
        if is_blog_post(post_json_object):
            edit_blog_post_str = 'Edit blog post'
            if translate.get(edit_blog_post_str):
                edit_blog_post_str = translate[edit_blog_post_str]
            if not is_news_post(post_json_object):
                edit_str += \
                    '        ' + \
                    '' + \
                    '
\n'
    return reply_str
def _get_edit_icon_html(base_dir: str, nickname: str, domain_full: str,
                        post_json_object: {}, actor_nickname: str,
                        translate: {}, is_event: bool) -> str:
    """Returns html for the edit icon/button
    """
    edit_str = ''
    actor = post_json_object['actor']
    # This should either be a post which you created,
    # or it could be generated from the newswire (see
    # _add_blogs_to_newswire) in which case anyone with
    # editor status should be able to alter it
    if (actor.endswith('/' + domain_full + '/users/' + nickname) or
        (is_editor(base_dir, nickname) and
         actor.endswith('/' + domain_full + '/users/news'))):
        post_id = remove_id_ending(post_json_object['object']['id'])
        if '/statuses/' not in post_id:
            return edit_str
        if is_blog_post(post_json_object):
            edit_blog_post_str = 'Edit blog post'
            if translate.get(edit_blog_post_str):
                edit_blog_post_str = translate[edit_blog_post_str]
            if not is_news_post(post_json_object):
                edit_str += \
                    '        ' + \
                    '' + \
                    ' \n'
            else:
                edit_str += \
                    '        ' + \
                    '' + \
                    '
\n'
            else:
                edit_str += \
                    '        ' + \
                    '' + \
                    ' \n'
        elif is_event:
            edit_event_str = 'Edit event'
            if translate.get(edit_event_str):
                edit_event_str = translate[edit_event_str]
            edit_str += \
                '        ' + \
                '' + \
                '
\n'
        elif is_event:
            edit_event_str = 'Edit event'
            if translate.get(edit_event_str):
                edit_event_str = translate[edit_event_str]
            edit_str += \
                '        ' + \
                '' + \
                ' \n'
    return edit_str
def _get_announce_icon_html(is_announced: bool,
                            post_actor: str,
                            nickname: str, domain_full: str,
                            announce_json_object: {},
                            post_json_object: {},
                            is_public_repeat: bool,
                            is_moderation_post: bool,
                            show_repeat_icon: bool,
                            translate: {},
                            page_number_param: str,
                            timeline_post_bookmark: str,
                            box_name: str,
                            max_announce_count: int) -> str:
    """Returns html for announce icon/button at the bottom of the post
    """
    announce_str = ''
    if not show_repeat_icon:
        return announce_str
    if is_moderation_post:
        return announce_str
    # don't allow announce/repeat of your own posts
    announce_icon = 'repeat_inactive.png'
    announce_link = 'repeat'
    announce_emoji = ''
    if not is_public_repeat:
        announce_link = 'repeatprivate'
    repeat_this_post_str = 'Repeat this post'
    if translate.get(repeat_this_post_str):
        repeat_this_post_str = translate[repeat_this_post_str]
    announce_title = repeat_this_post_str
    unannounce_link_str = ''
    announce_count = no_of_announces(post_json_object)
    announce_count_str = ''
    if announce_count > 0:
        if announce_count <= max_announce_count:
            announce_count_str = ' (' + str(announce_count) + ')'
        else:
            announce_count_str = ' (' + str(max_announce_count) + '+)'
    if announced_by_person(is_announced,
                           post_actor, nickname, domain_full):
        if announce_count == 1:
            # announced by the reader only
            announce_count_str = ''
        announce_icon = 'repeat.png'
        announce_emoji = '🔁 '
        announce_link = 'unrepeat'
        if not is_public_repeat:
            announce_link = 'unrepeatprivate'
        undo_the_repeat_str = 'Undo the repeat'
        if translate.get(undo_the_repeat_str):
            undo_the_repeat_str = translate[undo_the_repeat_str]
        announce_title = undo_the_repeat_str
        if announce_json_object:
            unannounce_link_str = '?unannounce=' + \
                remove_id_ending(announce_json_object['id'])
    announce_post_id = \
        remove_hash_from_post_id(post_json_object['object']['id'])
    announce_post_id = remove_id_ending(announce_post_id)
    announce_str = ''
    if announce_count_str:
        announcers_post_id = announce_post_id.replace('/', '--')
        announcers_screen_link = \
            '/users/' + nickname + '?announcers=' + announcers_post_id
        # show the number of announces next to icon
        announce_str += '\n'
    announce_link_str = '?' + \
        announce_link + '=' + announce_post_id + page_number_param
    announce_str += \
        '        \n'
    announce_str += \
        '          ' + \
        '
\n'
    return edit_str
def _get_announce_icon_html(is_announced: bool,
                            post_actor: str,
                            nickname: str, domain_full: str,
                            announce_json_object: {},
                            post_json_object: {},
                            is_public_repeat: bool,
                            is_moderation_post: bool,
                            show_repeat_icon: bool,
                            translate: {},
                            page_number_param: str,
                            timeline_post_bookmark: str,
                            box_name: str,
                            max_announce_count: int) -> str:
    """Returns html for announce icon/button at the bottom of the post
    """
    announce_str = ''
    if not show_repeat_icon:
        return announce_str
    if is_moderation_post:
        return announce_str
    # don't allow announce/repeat of your own posts
    announce_icon = 'repeat_inactive.png'
    announce_link = 'repeat'
    announce_emoji = ''
    if not is_public_repeat:
        announce_link = 'repeatprivate'
    repeat_this_post_str = 'Repeat this post'
    if translate.get(repeat_this_post_str):
        repeat_this_post_str = translate[repeat_this_post_str]
    announce_title = repeat_this_post_str
    unannounce_link_str = ''
    announce_count = no_of_announces(post_json_object)
    announce_count_str = ''
    if announce_count > 0:
        if announce_count <= max_announce_count:
            announce_count_str = ' (' + str(announce_count) + ')'
        else:
            announce_count_str = ' (' + str(max_announce_count) + '+)'
    if announced_by_person(is_announced,
                           post_actor, nickname, domain_full):
        if announce_count == 1:
            # announced by the reader only
            announce_count_str = ''
        announce_icon = 'repeat.png'
        announce_emoji = '🔁 '
        announce_link = 'unrepeat'
        if not is_public_repeat:
            announce_link = 'unrepeatprivate'
        undo_the_repeat_str = 'Undo the repeat'
        if translate.get(undo_the_repeat_str):
            undo_the_repeat_str = translate[undo_the_repeat_str]
        announce_title = undo_the_repeat_str
        if announce_json_object:
            unannounce_link_str = '?unannounce=' + \
                remove_id_ending(announce_json_object['id'])
    announce_post_id = \
        remove_hash_from_post_id(post_json_object['object']['id'])
    announce_post_id = remove_id_ending(announce_post_id)
    announce_str = ''
    if announce_count_str:
        announcers_post_id = announce_post_id.replace('/', '--')
        announcers_screen_link = \
            '/users/' + nickname + '?announcers=' + announcers_post_id
        # show the number of announces next to icon
        announce_str += '\n'
    announce_link_str = '?' + \
        announce_link + '=' + announce_post_id + page_number_param
    announce_str += \
        '        \n'
    announce_str += \
        '          ' + \
        '\n'
    return announce_str
def _get_like_icon_html(nickname: str, domain_full: str,
                        is_moderation_post: bool,
                        show_like_button: bool,
                        post_json_object: {},
                        enable_timing_log: bool,
                        post_start_time,
                        translate: {}, page_number_param: str,
                        timeline_post_bookmark: str,
                        box_name: str,
                        max_like_count: int) -> str:
    """Returns html for like icon/button
    """
    if not show_like_button or is_moderation_post:
        return ''
    like_str = ''
    like_icon = 'like_inactive.png'
    like_link = 'like'
    like_title = 'Like this post'
    if translate.get(like_title):
        like_title = translate[like_title]
    like_emoji = ''
    like_count = no_of_likes(post_json_object)
    _log_post_timing(enable_timing_log, post_start_time, '12.1')
    like_count_str = ''
    if like_count > 0:
        if like_count <= max_like_count:
            like_count_str = ' (' + str(like_count) + ')'
        else:
            like_count_str = ' (' + str(max_like_count) + '+)'
        if liked_by_person(post_json_object, nickname, domain_full):
            if like_count == 1:
                # liked by the reader only
                like_count_str = ''
            like_icon = 'like.png'
            like_link = 'unlike'
            like_title = 'Undo the like'
            if translate.get(like_title):
                like_title = translate[like_title]
            like_emoji = '👍 '
    _log_post_timing(enable_timing_log, post_start_time, '12.2')
    like_post_id = remove_hash_from_post_id(post_json_object['id'])
    like_post_id = remove_id_ending(like_post_id)
    like_str = ''
    if like_count_str:
        likers_post_id = like_post_id.replace('/', '--')
        likers_screen_link = \
            '/users/' + nickname + '?likers=' + likers_post_id
        # show the number of likes next to icon
        like_str += '\n'
    like_str += \
        '        \n'
    like_str += \
        '          ' + \
        '
\n'
    return like_str
def _get_bookmark_icon_html(nickname: str, domain_full: str,
                            post_json_object: {},
                            is_moderation_post: bool,
                            translate: {},
                            enable_timing_log: bool,
                            post_start_time, box_name: str,
                            page_number_param: str,
                            timeline_post_bookmark: str) -> str:
    """Returns html for bookmark icon/button
    """
    bookmark_str = ''
    if is_moderation_post:
        return bookmark_str
    bookmark_icon = 'bookmark_inactive.png'
    bookmark_link = 'bookmark'
    bookmark_emoji = ''
    bookmark_title = 'Bookmark this post'
    if translate.get(bookmark_title):
        bookmark_title = translate[bookmark_title]
    if bookmarked_by_person(post_json_object, nickname, domain_full):
        bookmark_icon = 'bookmark.png'
        bookmark_link = 'unbookmark'
        bookmark_emoji = '🔖 '
        bookmark_title = 'Undo the bookmark'
        if translate.get(bookmark_title):
            bookmark_title = translate[bookmark_title]
    _log_post_timing(enable_timing_log, post_start_time, '12.6')
    bookmark_post_id = \
        remove_hash_from_post_id(post_json_object['object']['id'])
    bookmark_post_id = remove_id_ending(bookmark_post_id)
    bookmark_str = \
        '        \n'
    bookmark_str += \
        '        ' + \
        '
\n'
    return bookmark_str
def _get_reaction_icon_html(nickname: str, post_json_object: {},
                            is_moderation_post: bool,
                            show_reaction_button: bool,
                            translate: {},
                            enable_timing_log: bool,
                            post_start_time, box_name: str,
                            page_number_param: str,
                            timeline_post_reaction: str) -> str:
    """Returns html for reaction icon/button
    """
    reaction_str = ''
    if not show_reaction_button or is_moderation_post:
        return reaction_str
    reaction_icon = 'reaction.png'
    reaction_title = 'Select reaction'
    if translate.get(reaction_title):
        reaction_title = translate[reaction_title]
    _log_post_timing(enable_timing_log, post_start_time, '12.65')
    reaction_post_id = \
        remove_hash_from_post_id(post_json_object['object']['id'])
    reaction_post_id = remove_id_ending(reaction_post_id)
    reaction_str = \
        '        \n'
    reaction_str += \
        '        ' + \
        '
\n'
    return reaction_str
def _get_mute_icon_html(is_muted: bool,
                        post_actor: str,
                        message_id: str,
                        nickname: str, domain_full: str,
                        allow_deletion: bool,
                        page_number_param: str,
                        box_name: str,
                        timeline_post_bookmark: str,
                        translate: {}) -> str:
    """Returns html for mute icon/button
    """
    mute_str = ''
    if (allow_deletion or
        ('/' + domain_full + '/' in post_actor and
         message_id.startswith(post_actor))):
        return mute_str
    if not is_muted:
        mute_this_post_str = 'Mute this post'
        if translate.get('Mute this post'):
            mute_this_post_str = translate[mute_this_post_str]
        mute_str = \
            '        \n'
        mute_str += \
            '          ' + \
            '
 \n'
    else:
        undo_mute_str = 'Undo mute'
        if translate.get(undo_mute_str):
            undo_mute_str = translate[undo_mute_str]
        mute_str = \
            '        \n'
        mute_str += \
            '          ' + \
            '
\n'
    else:
        undo_mute_str = 'Undo mute'
        if translate.get(undo_mute_str):
            undo_mute_str = translate[undo_mute_str]
        mute_str = \
            '        \n'
        mute_str += \
            '          ' + \
            ' \n'
    return mute_str
def _get_delete_icon_html(nickname: str, domain_full: str,
                          allow_deletion: bool,
                          post_actor: str,
                          message_id: str,
                          post_json_object: {},
                          page_number_param: str,
                          translate: {}) -> str:
    """Returns html for delete icon/button
    """
    delete_str = ''
    if (allow_deletion or
        ('/' + domain_full + '/' in post_actor and
         message_id.startswith(post_actor))):
        if '/users/' + nickname + '/' in message_id:
            if not is_news_post(post_json_object):
                delete_this_post_str = 'Delete this post'
                if translate.get(delete_this_post_str):
                    delete_this_post_str = translate[delete_this_post_str]
                delete_str = \
                    '        \n'
                delete_str += \
                    '          ' + \
                    '
\n'
    return mute_str
def _get_delete_icon_html(nickname: str, domain_full: str,
                          allow_deletion: bool,
                          post_actor: str,
                          message_id: str,
                          post_json_object: {},
                          page_number_param: str,
                          translate: {}) -> str:
    """Returns html for delete icon/button
    """
    delete_str = ''
    if (allow_deletion or
        ('/' + domain_full + '/' in post_actor and
         message_id.startswith(post_actor))):
        if '/users/' + nickname + '/' in message_id:
            if not is_news_post(post_json_object):
                delete_this_post_str = 'Delete this post'
                if translate.get(delete_this_post_str):
                    delete_this_post_str = translate[delete_this_post_str]
                delete_str = \
                    '        \n'
                delete_str += \
                    '          ' + \
                    ' \n'
    return delete_str
def _get_published_date_str(post_json_object: {},
                            show_published_date_only: bool,
                            timezone: str) -> str:
    """Return the html for the published date on a post
    """
    published_str = ''
    if not post_json_object['object'].get('published'):
        return published_str
    published_str = post_json_object['object']['published']
    if '.' not in published_str:
        if '+' not in published_str:
            datetime_object = \
                datetime.strptime(published_str, "%Y-%m-%dT%H:%M:%SZ")
        else:
            datetime_object = \
                datetime.strptime(published_str.split('+')[0] + 'Z',
                                  "%Y-%m-%dT%H:%M:%SZ")
    else:
        published_str = \
            published_str.replace('T', ' ').split('.')[0]
        datetime_object = parse(published_str)
    # convert to local time
    datetime_object = \
        convert_published_to_local_timezone(datetime_object, timezone)
    if not show_published_date_only:
        published_str = datetime_object.strftime("%a %b %d, %H:%M")
    else:
        published_str = datetime_object.strftime("%a %b %d")
    # if the post has replies then append a symbol to indicate this
    if post_json_object.get('hasReplies'):
        if post_json_object['hasReplies'] is True:
            published_str = '[' + published_str + ']'
    return published_str
def _get_blog_citations_html(box_name: str,
                             post_json_object: {},
                             translate: {}) -> str:
    """Returns blog citations as html
    """
    # show blog citations
    citations_str = ''
    if box_name not in ('tlblogs', 'tlfeatures'):
        return citations_str
    if not post_json_object['object'].get('tag'):
        return citations_str
    for tag_json in post_json_object['object']['tag']:
        if not isinstance(tag_json, dict):
            continue
        if not tag_json.get('type'):
            continue
        if tag_json['type'] != 'Article':
            continue
        if not tag_json.get('name'):
            continue
        if not tag_json.get('url'):
            continue
        citations_str += \
            '
\n'
    return delete_str
def _get_published_date_str(post_json_object: {},
                            show_published_date_only: bool,
                            timezone: str) -> str:
    """Return the html for the published date on a post
    """
    published_str = ''
    if not post_json_object['object'].get('published'):
        return published_str
    published_str = post_json_object['object']['published']
    if '.' not in published_str:
        if '+' not in published_str:
            datetime_object = \
                datetime.strptime(published_str, "%Y-%m-%dT%H:%M:%SZ")
        else:
            datetime_object = \
                datetime.strptime(published_str.split('+')[0] + 'Z',
                                  "%Y-%m-%dT%H:%M:%SZ")
    else:
        published_str = \
            published_str.replace('T', ' ').split('.')[0]
        datetime_object = parse(published_str)
    # convert to local time
    datetime_object = \
        convert_published_to_local_timezone(datetime_object, timezone)
    if not show_published_date_only:
        published_str = datetime_object.strftime("%a %b %d, %H:%M")
    else:
        published_str = datetime_object.strftime("%a %b %d")
    # if the post has replies then append a symbol to indicate this
    if post_json_object.get('hasReplies'):
        if post_json_object['hasReplies'] is True:
            published_str = '[' + published_str + ']'
    return published_str
def _get_blog_citations_html(box_name: str,
                             post_json_object: {},
                             translate: {}) -> str:
    """Returns blog citations as html
    """
    # show blog citations
    citations_str = ''
    if box_name not in ('tlblogs', 'tlfeatures'):
        return citations_str
    if not post_json_object['object'].get('tag'):
        return citations_str
    for tag_json in post_json_object['object']['tag']:
        if not isinstance(tag_json, dict):
            continue
        if not tag_json.get('type'):
            continue
        if tag_json['type'] != 'Article':
            continue
        if not tag_json.get('name'):
            continue
        if not tag_json.get('url'):
            continue
        citations_str += \
            ' \n'
def _announce_unattributed_html(translate: {},
                                post_json_object: {}) -> str:
    """Returns the html for an announce title where there
    is no attribution on the announced post
    """
    announces_str = 'announces'
    if translate.get(announces_str):
        announces_str = translate[announces_str]
    post_id = remove_id_ending(post_json_object['object']['id'])
    return '
\n'
def _announce_unattributed_html(translate: {},
                                post_json_object: {}) -> str:
    """Returns the html for an announce title where there
    is no attribution on the announced post
    """
    announces_str = 'announces'
    if translate.get(announces_str):
        announces_str = translate[announces_str]
    post_id = remove_id_ending(post_json_object['object']['id'])
    return '     \n' + \
        '      @unattributed\n'
def _announce_with_display_name_html(translate: {},
                                     post_json_object: {},
                                     announce_display_name: str) -> str:
    """Returns html for an announce having a display name
    """
    announces_str = 'announces'
    if translate.get(announces_str):
        announces_str = translate[announces_str]
    post_id = remove_id_ending(post_json_object['object']['id'])
    return '
\n' + \
        '      @unattributed\n'
def _announce_with_display_name_html(translate: {},
                                     post_json_object: {},
                                     announce_display_name: str) -> str:
    """Returns html for an announce having a display name
    """
    announces_str = 'announces'
    if translate.get(announces_str):
        announces_str = translate[announces_str]
    post_id = remove_id_ending(post_json_object['object']['id'])
    return '           \n' + \
        '        ' + \
        '' + \
        announce_display_name + '\n'
def _get_post_title_announce_html(base_dir: str,
                                  http_prefix: str,
                                  nickname: str, domain: str,
                                  show_repeat_icon: bool,
                                  is_announced: bool,
                                  post_json_object: {},
                                  post_actor: str,
                                  translate: {},
                                  enable_timing_log: bool,
                                  post_start_time,
                                  box_name: str,
                                  person_cache: {},
                                  allow_downloads: bool,
                                  avatar_position: str,
                                  page_number: int,
                                  message_id_str: str,
                                  container_class_icons: str,
                                  container_class: str,
                                  mitm: bool) -> (str, str, str, str):
    """Returns the announce title of a post containing names of participants
    x announces y
    """
    title_str = ''
    reply_avatar_image_in_post = ''
    obj_json = post_json_object['object']
    # has no attribution
    if not obj_json.get('attributedTo'):
        title_str += _announce_unattributed_html(translate, post_json_object)
        return (title_str, reply_avatar_image_in_post,
                container_class_icons, container_class)
    attributed_to = ''
    if isinstance(obj_json['attributedTo'], str):
        attributed_to = obj_json['attributedTo']
    # boosting your own post
    if attributed_to.startswith(post_actor):
        title_str += _boost_own_post_html(translate)
        return (title_str, reply_avatar_image_in_post,
                container_class_icons, container_class)
    # boosting another person's post
    _log_post_timing(enable_timing_log, post_start_time, '13.2')
    announce_nickname = None
    if attributed_to:
        announce_nickname = get_nickname_from_actor(attributed_to)
    if not announce_nickname:
        title_str += _announce_unattributed_html(translate, post_json_object)
        return (title_str, reply_avatar_image_in_post,
                container_class_icons, container_class)
    announce_domain, _ = get_domain_from_actor(attributed_to)
    get_person_from_cache(base_dir, attributed_to, person_cache)
    announce_display_name = \
        get_display_name(base_dir, attributed_to, person_cache)
    if announce_display_name:
        if len(announce_display_name) < 2 or \
           display_name_is_emoji(announce_display_name):
            announce_display_name = None
    if not announce_display_name:
        announce_display_name = announce_nickname + '@' + announce_domain
    _log_post_timing(enable_timing_log, post_start_time, '13.3')
    # add any emoji to the display name
    if ':' in announce_display_name:
        announce_display_name = \
            add_emoji_to_display_name(None, base_dir, http_prefix,
                                      nickname, domain,
                                      announce_display_name, False)
    _log_post_timing(enable_timing_log, post_start_time, '13.3.1')
    title_str += \
        _announce_with_display_name_html(translate, post_json_object,
                                         announce_display_name)
    if mitm:
        title_str += _mitm_warning_html(translate)
    # show avatar of person replied to
    announce_actor = attributed_to
    announce_avatar_url = \
        get_person_avatar_url(base_dir, announce_actor, person_cache)
    _log_post_timing(enable_timing_log, post_start_time, '13.4')
    if not announce_avatar_url:
        announce_avatar_url = ''
    idx = 'Show options for this person'
    if '/users/news/' not in announce_avatar_url:
        show_options_for_this_person_str = idx
        if translate.get(idx):
            show_options_for_this_person_str = translate[idx]
        reply_avatar_image_in_post = \
            '
\n' + \
        '        ' + \
        '' + \
        announce_display_name + '\n'
def _get_post_title_announce_html(base_dir: str,
                                  http_prefix: str,
                                  nickname: str, domain: str,
                                  show_repeat_icon: bool,
                                  is_announced: bool,
                                  post_json_object: {},
                                  post_actor: str,
                                  translate: {},
                                  enable_timing_log: bool,
                                  post_start_time,
                                  box_name: str,
                                  person_cache: {},
                                  allow_downloads: bool,
                                  avatar_position: str,
                                  page_number: int,
                                  message_id_str: str,
                                  container_class_icons: str,
                                  container_class: str,
                                  mitm: bool) -> (str, str, str, str):
    """Returns the announce title of a post containing names of participants
    x announces y
    """
    title_str = ''
    reply_avatar_image_in_post = ''
    obj_json = post_json_object['object']
    # has no attribution
    if not obj_json.get('attributedTo'):
        title_str += _announce_unattributed_html(translate, post_json_object)
        return (title_str, reply_avatar_image_in_post,
                container_class_icons, container_class)
    attributed_to = ''
    if isinstance(obj_json['attributedTo'], str):
        attributed_to = obj_json['attributedTo']
    # boosting your own post
    if attributed_to.startswith(post_actor):
        title_str += _boost_own_post_html(translate)
        return (title_str, reply_avatar_image_in_post,
                container_class_icons, container_class)
    # boosting another person's post
    _log_post_timing(enable_timing_log, post_start_time, '13.2')
    announce_nickname = None
    if attributed_to:
        announce_nickname = get_nickname_from_actor(attributed_to)
    if not announce_nickname:
        title_str += _announce_unattributed_html(translate, post_json_object)
        return (title_str, reply_avatar_image_in_post,
                container_class_icons, container_class)
    announce_domain, _ = get_domain_from_actor(attributed_to)
    get_person_from_cache(base_dir, attributed_to, person_cache)
    announce_display_name = \
        get_display_name(base_dir, attributed_to, person_cache)
    if announce_display_name:
        if len(announce_display_name) < 2 or \
           display_name_is_emoji(announce_display_name):
            announce_display_name = None
    if not announce_display_name:
        announce_display_name = announce_nickname + '@' + announce_domain
    _log_post_timing(enable_timing_log, post_start_time, '13.3')
    # add any emoji to the display name
    if ':' in announce_display_name:
        announce_display_name = \
            add_emoji_to_display_name(None, base_dir, http_prefix,
                                      nickname, domain,
                                      announce_display_name, False)
    _log_post_timing(enable_timing_log, post_start_time, '13.3.1')
    title_str += \
        _announce_with_display_name_html(translate, post_json_object,
                                         announce_display_name)
    if mitm:
        title_str += _mitm_warning_html(translate)
    # show avatar of person replied to
    announce_actor = attributed_to
    announce_avatar_url = \
        get_person_avatar_url(base_dir, announce_actor, person_cache)
    _log_post_timing(enable_timing_log, post_start_time, '13.4')
    if not announce_avatar_url:
        announce_avatar_url = ''
    idx = 'Show options for this person'
    if '/users/news/' not in announce_avatar_url:
        show_options_for_this_person_str = idx
        if translate.get(idx):
            show_options_for_this_person_str = translate[idx]
        reply_avatar_image_in_post = \
            '        \n    
 \n'
    return title_str
def _reply_to_unknown_html(translate: {},
                           post_json_object: {}) -> str:
    """Returns the html title for a reply to an unknown handle
    """
    replying_to_str = 'replying to'
    if translate.get(replying_to_str):
        replying_to_str = translate[replying_to_str]
    return '
\n'
    return title_str
def _reply_to_unknown_html(translate: {},
                           post_json_object: {}) -> str:
    """Returns the html title for a reply to an unknown handle
    """
    replying_to_str = 'replying to'
    if translate.get(replying_to_str):
        replying_to_str = translate[replying_to_str]
    return '         \n' + \
        '        @unknown\n'
def _mitm_warning_html(translate: {}) -> str:
    """Returns the html title for a reply to an unknown handle
    """
    mitm_warning_str = translate['mitm']
    return '
\n' + \
        '        @unknown\n'
def _mitm_warning_html(translate: {}) -> str:
    """Returns the html title for a reply to an unknown handle
    """
    mitm_warning_str = translate['mitm']
    return '         \n'
def _reply_with_unknown_path_html(translate: {},
                                  post_json_object: {},
                                  post_domain: str) -> str:
    """Returns html title for a reply with an unknown path
    eg. does not contain /statuses/
    """
    replying_to_str = 'replying to'
    if translate.get(replying_to_str):
        replying_to_str = translate[replying_to_str]
    return '
\n'
def _reply_with_unknown_path_html(translate: {},
                                  post_json_object: {},
                                  post_domain: str) -> str:
    """Returns html title for a reply with an unknown path
    eg. does not contain /statuses/
    """
    replying_to_str = 'replying to'
    if translate.get(replying_to_str):
        replying_to_str = translate[replying_to_str]
    return '         \n' + \
        '        ' + \
        post_domain + '\n'
def _get_reply_html(translate: {},
                    in_reply_to: str, reply_display_name: str) -> str:
    """Returns html title for a reply
    """
    replying_to_str = 'replying to'
    if translate.get(replying_to_str):
        replying_to_str = translate[replying_to_str]
    return '        ' + \
        '
\n' + \
        '        ' + \
        post_domain + '\n'
def _get_reply_html(translate: {},
                    in_reply_to: str, reply_display_name: str) -> str:
    """Returns html title for a reply
    """
    replying_to_str = 'replying to'
    if translate.get(replying_to_str):
        replying_to_str = translate[replying_to_str]
    return '        ' + \
        ' \n' + \
        '        ' + \
        '' + \
        reply_display_name + '\n'
def _get_post_title_reply_html(base_dir: str,
                               http_prefix: str,
                               nickname: str, domain: str,
                               show_repeat_icon: bool,
                               is_announced: bool,
                               post_json_object: {},
                               post_actor: str,
                               translate: {},
                               enable_timing_log: bool,
                               post_start_time,
                               box_name: str,
                               person_cache: {},
                               allow_downloads: bool,
                               avatar_position: str,
                               page_number: int,
                               message_id_str: str,
                               container_class_icons: str,
                               container_class: str,
                               mitm: bool) -> (str, str, str, str):
    """Returns the reply title of a post containing names of participants
    x replies to y
    """
    title_str = ''
    reply_avatar_image_in_post = ''
    obj_json = post_json_object['object']
    # not a reply
    if not obj_json.get('inReplyTo'):
        return (title_str, reply_avatar_image_in_post,
                container_class_icons, container_class)
    container_class_icons = 'containericons darker'
    container_class = 'container darker'
    # reply to self
    if obj_json['inReplyTo'].startswith(post_actor):
        title_str += _reply_to_yourself_html(translate)
        return (title_str, reply_avatar_image_in_post,
                container_class_icons, container_class)
    # has a reply
    if '/statuses/' not in obj_json['inReplyTo']:
        post_domain = obj_json['inReplyTo']
        prefixes = get_protocol_prefixes()
        for prefix in prefixes:
            post_domain = post_domain.replace(prefix, '')
        if '/' in post_domain:
            post_domain = post_domain.split('/', 1)[0]
        if post_domain:
            title_str += \
                _reply_with_unknown_path_html(translate,
                                              post_json_object, post_domain)
        return (title_str, reply_avatar_image_in_post,
                container_class_icons, container_class)
    in_reply_to = obj_json['inReplyTo']
    reply_actor = in_reply_to.split('/statuses/')[0]
    reply_nickname = get_nickname_from_actor(reply_actor)
    if not reply_nickname:
        title_str += _reply_to_unknown_html(translate, post_json_object)
        return (title_str, reply_avatar_image_in_post,
                container_class_icons, container_class)
    reply_domain, _ = get_domain_from_actor(reply_actor)
    if not (reply_nickname and reply_domain):
        title_str += _reply_to_unknown_html(translate, post_json_object)
        return (title_str, reply_avatar_image_in_post,
                container_class_icons, container_class)
    get_person_from_cache(base_dir, reply_actor, person_cache)
    reply_display_name = \
        get_display_name(base_dir, reply_actor, person_cache)
    if reply_display_name:
        if len(reply_display_name) < 2 or \
           display_name_is_emoji(reply_display_name):
            reply_display_name = None
    if not reply_display_name:
        reply_display_name = reply_nickname + '@' + reply_domain
    # add emoji to the display name
    if ':' in reply_display_name:
        _log_post_timing(enable_timing_log, post_start_time, '13.5')
        reply_display_name = \
            add_emoji_to_display_name(None, base_dir, http_prefix,
                                      nickname, domain,
                                      reply_display_name, False)
        _log_post_timing(enable_timing_log, post_start_time, '13.6')
    title_str += _get_reply_html(translate, in_reply_to, reply_display_name)
    if mitm:
        title_str += _mitm_warning_html(translate)
    _log_post_timing(enable_timing_log, post_start_time, '13.7')
    # show avatar of person replied to
    reply_avatar_url = \
        get_person_avatar_url(base_dir, reply_actor, person_cache)
    _log_post_timing(enable_timing_log, post_start_time, '13.8')
    if reply_avatar_url:
        show_profile_str = 'Show profile'
        if translate.get(show_profile_str):
            show_profile_str = translate[show_profile_str]
        reply_avatar_image_in_post = \
            '
\n' + \
        '        ' + \
        '' + \
        reply_display_name + '\n'
def _get_post_title_reply_html(base_dir: str,
                               http_prefix: str,
                               nickname: str, domain: str,
                               show_repeat_icon: bool,
                               is_announced: bool,
                               post_json_object: {},
                               post_actor: str,
                               translate: {},
                               enable_timing_log: bool,
                               post_start_time,
                               box_name: str,
                               person_cache: {},
                               allow_downloads: bool,
                               avatar_position: str,
                               page_number: int,
                               message_id_str: str,
                               container_class_icons: str,
                               container_class: str,
                               mitm: bool) -> (str, str, str, str):
    """Returns the reply title of a post containing names of participants
    x replies to y
    """
    title_str = ''
    reply_avatar_image_in_post = ''
    obj_json = post_json_object['object']
    # not a reply
    if not obj_json.get('inReplyTo'):
        return (title_str, reply_avatar_image_in_post,
                container_class_icons, container_class)
    container_class_icons = 'containericons darker'
    container_class = 'container darker'
    # reply to self
    if obj_json['inReplyTo'].startswith(post_actor):
        title_str += _reply_to_yourself_html(translate)
        return (title_str, reply_avatar_image_in_post,
                container_class_icons, container_class)
    # has a reply
    if '/statuses/' not in obj_json['inReplyTo']:
        post_domain = obj_json['inReplyTo']
        prefixes = get_protocol_prefixes()
        for prefix in prefixes:
            post_domain = post_domain.replace(prefix, '')
        if '/' in post_domain:
            post_domain = post_domain.split('/', 1)[0]
        if post_domain:
            title_str += \
                _reply_with_unknown_path_html(translate,
                                              post_json_object, post_domain)
        return (title_str, reply_avatar_image_in_post,
                container_class_icons, container_class)
    in_reply_to = obj_json['inReplyTo']
    reply_actor = in_reply_to.split('/statuses/')[0]
    reply_nickname = get_nickname_from_actor(reply_actor)
    if not reply_nickname:
        title_str += _reply_to_unknown_html(translate, post_json_object)
        return (title_str, reply_avatar_image_in_post,
                container_class_icons, container_class)
    reply_domain, _ = get_domain_from_actor(reply_actor)
    if not (reply_nickname and reply_domain):
        title_str += _reply_to_unknown_html(translate, post_json_object)
        return (title_str, reply_avatar_image_in_post,
                container_class_icons, container_class)
    get_person_from_cache(base_dir, reply_actor, person_cache)
    reply_display_name = \
        get_display_name(base_dir, reply_actor, person_cache)
    if reply_display_name:
        if len(reply_display_name) < 2 or \
           display_name_is_emoji(reply_display_name):
            reply_display_name = None
    if not reply_display_name:
        reply_display_name = reply_nickname + '@' + reply_domain
    # add emoji to the display name
    if ':' in reply_display_name:
        _log_post_timing(enable_timing_log, post_start_time, '13.5')
        reply_display_name = \
            add_emoji_to_display_name(None, base_dir, http_prefix,
                                      nickname, domain,
                                      reply_display_name, False)
        _log_post_timing(enable_timing_log, post_start_time, '13.6')
    title_str += _get_reply_html(translate, in_reply_to, reply_display_name)
    if mitm:
        title_str += _mitm_warning_html(translate)
    _log_post_timing(enable_timing_log, post_start_time, '13.7')
    # show avatar of person replied to
    reply_avatar_url = \
        get_person_avatar_url(base_dir, reply_actor, person_cache)
    _log_post_timing(enable_timing_log, post_start_time, '13.8')
    if reply_avatar_url:
        show_profile_str = 'Show profile'
        if translate.get(show_profile_str):
            show_profile_str = translate[show_profile_str]
        reply_avatar_image_in_post = \
            '        \n        
 \n'
    # check if replying is permitted
    comments_enabled = True
    if isinstance(post_json_object['object'], dict) and \
       'commentsEnabled' in post_json_object['object']:
        if post_json_object['object']['commentsEnabled'] is False:
            comments_enabled = False
        elif 'rejectReplies' in post_json_object['object']:
            if post_json_object['object']['rejectReplies']:
                comments_enabled = False
    conversation_id = None
    if isinstance(post_json_object['object'], dict) and \
       'conversation' in post_json_object['object']:
        if post_json_object['object']['conversation']:
            conversation_id = post_json_object['object']['conversation']
    public_reply = False
    unlisted_reply = False
    if is_public_post(post_json_object):
        public_reply = True
    if is_unlisted_post(post_json_object):
        public_reply = False
        unlisted_reply = True
    reply_str = _get_reply_icon_html(base_dir, nickname, domain,
                                     public_reply, unlisted_reply,
                                     show_icons, comments_enabled,
                                     post_json_object, page_number_param,
                                     translate, system_language,
                                     conversation_id)
    _log_post_timing(enable_timing_log, post_start_time, '10')
    edit_str = _get_edit_icon_html(base_dir, nickname, domain_full,
                                   post_json_object, actor_nickname,
                                   translate, False)
    _log_post_timing(enable_timing_log, post_start_time, '11')
    announce_str = \
        _get_announce_icon_html(is_announced,
                                post_actor,
                                nickname, domain_full,
                                announce_json_object,
                                post_json_object,
                                is_public_repeat,
                                is_moderation_post,
                                show_repeat_icon,
                                translate,
                                page_number_param,
                                timeline_post_bookmark,
                                box_name, max_like_count)
    _log_post_timing(enable_timing_log, post_start_time, '12')
    # whether to show a like button
    hide_like_button_file = \
        acct_dir(base_dir, nickname, domain) + '/.hideLikeButton'
    show_like_button = True
    if os.path.isfile(hide_like_button_file):
        show_like_button = False
    # whether to show a reaction button
    hide_reaction_button_file = \
        acct_dir(base_dir, nickname, domain) + '/.hideReactionButton'
    show_reaction_button = True
    if os.path.isfile(hide_reaction_button_file):
        show_reaction_button = False
    like_json_object = post_json_object
    if announce_json_object:
        like_json_object = announce_json_object
    like_str = _get_like_icon_html(nickname, domain_full,
                                   is_moderation_post,
                                   show_like_button,
                                   like_json_object,
                                   enable_timing_log,
                                   post_start_time,
                                   translate, page_number_param,
                                   timeline_post_bookmark,
                                   box_name, max_like_count)
    _log_post_timing(enable_timing_log, post_start_time, '12.5')
    bookmark_str = \
        _get_bookmark_icon_html(nickname, domain_full,
                                post_json_object,
                                is_moderation_post,
                                translate,
                                enable_timing_log,
                                post_start_time, box_name,
                                page_number_param,
                                timeline_post_bookmark)
    _log_post_timing(enable_timing_log, post_start_time, '12.9')
    reaction_str = \
        _get_reaction_icon_html(nickname, post_json_object,
                                is_moderation_post,
                                show_reaction_button,
                                translate,
                                enable_timing_log,
                                post_start_time, box_name,
                                page_number_param,
                                timeline_post_bookmark)
    _log_post_timing(enable_timing_log, post_start_time, '12.10')
    is_muted = post_is_muted(base_dir, nickname, domain,
                             post_json_object, message_id)
    _log_post_timing(enable_timing_log, post_start_time, '13')
    mute_str = \
        _get_mute_icon_html(is_muted,
                            post_actor,
                            message_id,
                            nickname, domain_full,
                            allow_deletion,
                            page_number_param,
                            box_name,
                            timeline_post_bookmark,
                            translate)
    delete_str = \
        _get_delete_icon_html(nickname, domain_full,
                              allow_deletion,
                              post_actor,
                              message_id,
                              post_json_object,
                              page_number_param,
                              translate)
    _log_post_timing(enable_timing_log, post_start_time, '13.1')
    # get the title: x replies to y, x announces y, etc
    (title_str2,
     reply_avatar_image_in_post,
     container_class_icons,
     container_class) = _get_post_title_html(base_dir,
                                             http_prefix,
                                             nickname, domain,
                                             show_repeat_icon,
                                             is_announced,
                                             post_json_object,
                                             post_actor,
                                             translate,
                                             enable_timing_log,
                                             post_start_time,
                                             box_name,
                                             person_cache,
                                             allow_downloads,
                                             avatar_position,
                                             page_number,
                                             message_id_str,
                                             container_class_icons,
                                             container_class, mitm)
    title_str += title_str2
    _log_post_timing(enable_timing_log, post_start_time, '14')
    person_url = local_actor_url(http_prefix, nickname, domain_full)
    actor_json = \
        get_person_from_cache(base_dir, person_url, person_cache)
    languages_understood = []
    if actor_json:
        languages_understood = get_actor_languages_list(actor_json)
    content_str = get_content_from_post(post_json_object, system_language,
                                        languages_understood)
    attachment_str, gallery_str = \
        get_post_attachments_as_html(base_dir, nickname, domain,
                                     domain_full,
                                     post_json_object,
                                     box_name, translate,
                                     is_muted, avatar_link,
                                     reply_str, announce_str, like_str,
                                     bookmark_str, delete_str, mute_str,
                                     content_str)
    published_str = \
        _get_published_date_str(post_json_object, show_published_date_only,
                                timezone)
    _log_post_timing(enable_timing_log, post_start_time, '15')
    published_link = message_id
    # blog posts should have no /statuses/ in their link
    post_is_blog = False
    if is_blog_post(post_json_object):
        post_is_blog = True
        # is this a post to the local domain?
        if '://' + domain in message_id:
            published_link = message_id.replace('/statuses/', '/')
    # if this is a local link then make it relative so that it works
    # on clearnet or onion address
    if domain + '/users/' in published_link or \
       domain + ':' + str(port) + '/users/' in published_link:
        published_link = '/users/' + published_link.split('/users/')[1]
    if not is_news_post(post_json_object):
        footer_str = '' + \
            published_str + '\n'
    else:
        footer_str = '' + \
            published_str + '\n'
    # change the background color for DMs in inbox timeline
    if post_is_dm:
        container_class_icons = 'containericons dm'
        container_class = 'container dm'
    # add any content warning from the cwlists directory
    add_cw_from_lists(post_json_object, cw_lists, translate, lists_enabled,
                      system_language)
    post_is_sensitive = False
    if post_json_object['object'].get('sensitive'):
        # sensitive posts should have a summary
        if post_json_object['object'].get('summary'):
            post_is_sensitive = post_json_object['object']['sensitive']
        else:
            # add a generic summary if none is provided
            sensitive_str = 'Sensitive'
            if translate.get(sensitive_str):
                sensitive_str = translate[sensitive_str]
            post_json_object['object']['summary'] = sensitive_str
            post_json_object['object']['summaryMap'] = {
                system_language: sensitive_str
            }
    if not post_json_object['object'].get('summary'):
        post_json_object['object']['summary'] = ''
        post_json_object['object']['summaryMap'] = {
            system_language: ''
        }
    displaying_ciphertext = False
    if post_json_object['object'].get('cipherText'):
        displaying_ciphertext = True
        post_json_object['object']['content'] = \
            e2e_edecrypt_message_from_device(post_json_object['object'])
        post_json_object['object']['contentMap'][system_language] = \
            post_json_object['object']['content']
    domain_full = get_full_domain(domain, port)
    if not content_str:
        content_str = get_content_from_post(post_json_object, system_language,
                                            languages_understood)
    if not content_str:
        content_str = \
            auto_translate_post(base_dir, post_json_object,
                                system_language, translate)
        if not content_str:
            return ''
    summary_str = ''
    if content_str:
        summary_str = get_summary_from_post(post_json_object, system_language,
                                            languages_understood)
        content_all_str = str(summary_str) + ' ' + content_str
        # does an emoji indicate a no boost preference?
        # if so then don't show the repeat/announce icon
        if disallow_announce(content_all_str):
            announce_str = ''
        # does an emoji indicate a no replies preference?
        # if so then don't show the reply icon
        if disallow_reply(content_all_str):
            reply_str = ''
    new_footer_str = \
        _get_footer_with_icons(show_icons,
                               container_class_icons,
                               reply_str, announce_str,
                               like_str, reaction_str, bookmark_str,
                               delete_str, mute_str, edit_str,
                               post_json_object, published_link,
                               time_class, published_str)
    if new_footer_str:
        footer_str = new_footer_str
    # add an extra line if there is a content warning,
    # for better vertical spacing on mobile
    if post_is_sensitive:
        footer_str = '
\n'
    # check if replying is permitted
    comments_enabled = True
    if isinstance(post_json_object['object'], dict) and \
       'commentsEnabled' in post_json_object['object']:
        if post_json_object['object']['commentsEnabled'] is False:
            comments_enabled = False
        elif 'rejectReplies' in post_json_object['object']:
            if post_json_object['object']['rejectReplies']:
                comments_enabled = False
    conversation_id = None
    if isinstance(post_json_object['object'], dict) and \
       'conversation' in post_json_object['object']:
        if post_json_object['object']['conversation']:
            conversation_id = post_json_object['object']['conversation']
    public_reply = False
    unlisted_reply = False
    if is_public_post(post_json_object):
        public_reply = True
    if is_unlisted_post(post_json_object):
        public_reply = False
        unlisted_reply = True
    reply_str = _get_reply_icon_html(base_dir, nickname, domain,
                                     public_reply, unlisted_reply,
                                     show_icons, comments_enabled,
                                     post_json_object, page_number_param,
                                     translate, system_language,
                                     conversation_id)
    _log_post_timing(enable_timing_log, post_start_time, '10')
    edit_str = _get_edit_icon_html(base_dir, nickname, domain_full,
                                   post_json_object, actor_nickname,
                                   translate, False)
    _log_post_timing(enable_timing_log, post_start_time, '11')
    announce_str = \
        _get_announce_icon_html(is_announced,
                                post_actor,
                                nickname, domain_full,
                                announce_json_object,
                                post_json_object,
                                is_public_repeat,
                                is_moderation_post,
                                show_repeat_icon,
                                translate,
                                page_number_param,
                                timeline_post_bookmark,
                                box_name, max_like_count)
    _log_post_timing(enable_timing_log, post_start_time, '12')
    # whether to show a like button
    hide_like_button_file = \
        acct_dir(base_dir, nickname, domain) + '/.hideLikeButton'
    show_like_button = True
    if os.path.isfile(hide_like_button_file):
        show_like_button = False
    # whether to show a reaction button
    hide_reaction_button_file = \
        acct_dir(base_dir, nickname, domain) + '/.hideReactionButton'
    show_reaction_button = True
    if os.path.isfile(hide_reaction_button_file):
        show_reaction_button = False
    like_json_object = post_json_object
    if announce_json_object:
        like_json_object = announce_json_object
    like_str = _get_like_icon_html(nickname, domain_full,
                                   is_moderation_post,
                                   show_like_button,
                                   like_json_object,
                                   enable_timing_log,
                                   post_start_time,
                                   translate, page_number_param,
                                   timeline_post_bookmark,
                                   box_name, max_like_count)
    _log_post_timing(enable_timing_log, post_start_time, '12.5')
    bookmark_str = \
        _get_bookmark_icon_html(nickname, domain_full,
                                post_json_object,
                                is_moderation_post,
                                translate,
                                enable_timing_log,
                                post_start_time, box_name,
                                page_number_param,
                                timeline_post_bookmark)
    _log_post_timing(enable_timing_log, post_start_time, '12.9')
    reaction_str = \
        _get_reaction_icon_html(nickname, post_json_object,
                                is_moderation_post,
                                show_reaction_button,
                                translate,
                                enable_timing_log,
                                post_start_time, box_name,
                                page_number_param,
                                timeline_post_bookmark)
    _log_post_timing(enable_timing_log, post_start_time, '12.10')
    is_muted = post_is_muted(base_dir, nickname, domain,
                             post_json_object, message_id)
    _log_post_timing(enable_timing_log, post_start_time, '13')
    mute_str = \
        _get_mute_icon_html(is_muted,
                            post_actor,
                            message_id,
                            nickname, domain_full,
                            allow_deletion,
                            page_number_param,
                            box_name,
                            timeline_post_bookmark,
                            translate)
    delete_str = \
        _get_delete_icon_html(nickname, domain_full,
                              allow_deletion,
                              post_actor,
                              message_id,
                              post_json_object,
                              page_number_param,
                              translate)
    _log_post_timing(enable_timing_log, post_start_time, '13.1')
    # get the title: x replies to y, x announces y, etc
    (title_str2,
     reply_avatar_image_in_post,
     container_class_icons,
     container_class) = _get_post_title_html(base_dir,
                                             http_prefix,
                                             nickname, domain,
                                             show_repeat_icon,
                                             is_announced,
                                             post_json_object,
                                             post_actor,
                                             translate,
                                             enable_timing_log,
                                             post_start_time,
                                             box_name,
                                             person_cache,
                                             allow_downloads,
                                             avatar_position,
                                             page_number,
                                             message_id_str,
                                             container_class_icons,
                                             container_class, mitm)
    title_str += title_str2
    _log_post_timing(enable_timing_log, post_start_time, '14')
    person_url = local_actor_url(http_prefix, nickname, domain_full)
    actor_json = \
        get_person_from_cache(base_dir, person_url, person_cache)
    languages_understood = []
    if actor_json:
        languages_understood = get_actor_languages_list(actor_json)
    content_str = get_content_from_post(post_json_object, system_language,
                                        languages_understood)
    attachment_str, gallery_str = \
        get_post_attachments_as_html(base_dir, nickname, domain,
                                     domain_full,
                                     post_json_object,
                                     box_name, translate,
                                     is_muted, avatar_link,
                                     reply_str, announce_str, like_str,
                                     bookmark_str, delete_str, mute_str,
                                     content_str)
    published_str = \
        _get_published_date_str(post_json_object, show_published_date_only,
                                timezone)
    _log_post_timing(enable_timing_log, post_start_time, '15')
    published_link = message_id
    # blog posts should have no /statuses/ in their link
    post_is_blog = False
    if is_blog_post(post_json_object):
        post_is_blog = True
        # is this a post to the local domain?
        if '://' + domain in message_id:
            published_link = message_id.replace('/statuses/', '/')
    # if this is a local link then make it relative so that it works
    # on clearnet or onion address
    if domain + '/users/' in published_link or \
       domain + ':' + str(port) + '/users/' in published_link:
        published_link = '/users/' + published_link.split('/users/')[1]
    if not is_news_post(post_json_object):
        footer_str = '' + \
            published_str + '\n'
    else:
        footer_str = '' + \
            published_str + '\n'
    # change the background color for DMs in inbox timeline
    if post_is_dm:
        container_class_icons = 'containericons dm'
        container_class = 'container dm'
    # add any content warning from the cwlists directory
    add_cw_from_lists(post_json_object, cw_lists, translate, lists_enabled,
                      system_language)
    post_is_sensitive = False
    if post_json_object['object'].get('sensitive'):
        # sensitive posts should have a summary
        if post_json_object['object'].get('summary'):
            post_is_sensitive = post_json_object['object']['sensitive']
        else:
            # add a generic summary if none is provided
            sensitive_str = 'Sensitive'
            if translate.get(sensitive_str):
                sensitive_str = translate[sensitive_str]
            post_json_object['object']['summary'] = sensitive_str
            post_json_object['object']['summaryMap'] = {
                system_language: sensitive_str
            }
    if not post_json_object['object'].get('summary'):
        post_json_object['object']['summary'] = ''
        post_json_object['object']['summaryMap'] = {
            system_language: ''
        }
    displaying_ciphertext = False
    if post_json_object['object'].get('cipherText'):
        displaying_ciphertext = True
        post_json_object['object']['content'] = \
            e2e_edecrypt_message_from_device(post_json_object['object'])
        post_json_object['object']['contentMap'][system_language] = \
            post_json_object['object']['content']
    domain_full = get_full_domain(domain, port)
    if not content_str:
        content_str = get_content_from_post(post_json_object, system_language,
                                            languages_understood)
    if not content_str:
        content_str = \
            auto_translate_post(base_dir, post_json_object,
                                system_language, translate)
        if not content_str:
            return ''
    summary_str = ''
    if content_str:
        summary_str = get_summary_from_post(post_json_object, system_language,
                                            languages_understood)
        content_all_str = str(summary_str) + ' ' + content_str
        # does an emoji indicate a no boost preference?
        # if so then don't show the repeat/announce icon
        if disallow_announce(content_all_str):
            announce_str = ''
        # does an emoji indicate a no replies preference?
        # if so then don't show the reply icon
        if disallow_reply(content_all_str):
            reply_str = ''
    new_footer_str = \
        _get_footer_with_icons(show_icons,
                               container_class_icons,
                               reply_str, announce_str,
                               like_str, reaction_str, bookmark_str,
                               delete_str, mute_str, edit_str,
                               post_json_object, published_link,
                               time_class, published_str)
    if new_footer_str:
        footer_str = new_footer_str
    # add an extra line if there is a content warning,
    # for better vertical spacing on mobile
    if post_is_sensitive:
        footer_str = '
' + footer_str
    if not summary_str:
        summary_str = get_summary_from_post(post_json_object, system_language,
                                            languages_understood)
    is_patch = is_git_patch(base_dir, nickname, domain,
                            post_json_object['object']['type'],
                            summary_str, content_str)
    _log_post_timing(enable_timing_log, post_start_time, '16')
    if not is_pgp_encrypted(content_str):
        # if we are on an onion instance then substitute any common clearnet
        # domains with their onion version
        if '.onion' in domain and '://' in content_str:
            content_str = \
                _substitute_onion_domains(base_dir, content_str)
        if not is_patch:
            # remove any tabs
            content_str = \
                content_str.replace('\t', '').replace('\r', '')
            # Add bold text
            if bold_reading and \
               not displaying_ciphertext and \
               not post_is_blog:
                content_str = bold_reading_string(content_str)
            object_content = \
                remove_long_words(content_str, 40, [])
            object_content = \
                remove_text_formatting(object_content, bold_reading)
            object_content = limit_repeated_words(object_content, 6)
            object_content = \
                switch_words(base_dir, nickname, domain, object_content)
            object_content = html_replace_email_quote(object_content)
            object_content = html_replace_quote_marks(object_content)
            # append any edits
            object_content += edits_str
        else:
            object_content = content_str
    else:
        encrypted_str = 'Encrypted'
        if translate.get(encrypted_str):
            encrypted_str = translate[encrypted_str]
        object_content = '🔒 ' + encrypted_str
    object_content = \
        '' + content_str + \
                '
' + reaction_str
        post_html = '