__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 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 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 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 def _html_post_metadata_open_graph(domain: str, post_json_object: {}) -> 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'): description = remove_html(obj_json['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'): description += '\n\n' + remove_html(obj_json['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, debugId: 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 ' + debugId + ' = ' + 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+') 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, allow_downloads) _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(showAvatarOptions: 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 """ 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 += \ ' \n' if showAvatarOptions 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, "\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 += \ ' ' + \ '' + reply_to_this_post_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: {}, isEvent: 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 += \ ' ' + \ '' + \ '' + edit_blog_post_str + \
                    ' |\n' else: edit_str += \ ' ' + \ '' + \ '' + edit_blog_post_str + \
                    ' |\n' elif isEvent: edit_event_str = 'Edit event' if translate.get(edit_event_str): edit_event_str = translate[edit_event_str] edit_str += \ ' ' + \ '' + \ '' + edit_event_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 += \ ' ' + \ '' + announce_emoji + announce_title + \
        ' |\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 += \ ' ' + \ '' + like_emoji + like_title + \
        ' |\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 += \ ' ' + \ '' + \
        bookmark_emoji + bookmark_title + ' |\n' return bookmark_str def _get_reaction_icon_html(nickname: str, domain_full: 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 += \ ' ' + \ '' + \
        reaction_title + ' |\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 += \ ' ' + \ '' + \
            mute_this_post_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 += \ ' ' + \ '' + \
                    delete_this_post_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 += \ '
  • ' + \ '' + tag_json['name'] + '
  • \n' if citations_str: translated_citations_str = 'Citations' if translate.get(translated_citations_str): translated_citations_str = translate[translated_citations_str] citations_str = '

    ' + translated_citations_str + ':

    ' + \ '\n' return citations_str def _boost_own_post_html(translate: {}) -> str: """The html title for announcing your own post """ announces_str = 'announces' if translate.get(announces_str): announces_str = translate[announces_str] return ' ' + announces_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 ' ' + \
        announces_str + '\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 ' ' + \
        announces_str + '\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, allow_downloads) announce_display_name = \ get_display_name(base_dir, attributed_to, person_cache) 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, allow_downloads) _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
    \n' return (title_str, reply_avatar_image_in_post, container_class_icons, container_class) def _reply_to_yourself_html(translate: {}) -> str: """Returns html for a title which is a reply to yourself """ replying_to_themselves_str = 'replying to themselves' if translate.get(replying_to_themselves_str): replying_to_themselves_str = translate[replying_to_themselves_str] return ' ' + replying_to_themselves_str + \
        '\n' 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 ' ' + \
        replying_to_str + '\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 ' ' + \
        mitm_warning_str + '\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 ' ' + replying_to_str + \
        '\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 ' ' + \ '' + \
        replying_to_str + '\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, allow_downloads) reply_display_name = get_display_name(base_dir, reply_actor, person_cache) 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, allow_downloads) _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' + \ '  \n
    \n' return (title_str, reply_avatar_image_in_post, container_class_icons, container_class) def _get_post_title_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 title of a post containing names of participants x replies to y, x announces y, etc """ if not is_announced and box_name == 'search' and \ post_json_object.get('object'): if post_json_object['object'].get('attributedTo'): if post_json_object['object']['attributedTo'] != post_actor: is_announced = True if is_announced: return _get_post_title_announce_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) return _get_post_title_reply_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) def _get_footer_with_icons(show_icons: bool, container_class_icons: str, reply_str: str, announce_str: str, like_str: str, reaction_str: str, bookmark_str: str, delete_str: str, mute_str: str, edit_str: str, post_json_object: {}, published_link: str, time_class: str, published_str: str) -> str: """Returns the html for a post footer containing icons """ if not show_icons: return None footer_str = '\n \n' return footer_str def individual_post_as_html(signing_priv_key_pem: str, allow_downloads: bool, recent_posts_cache: {}, max_recent_posts: int, translate: {}, page_number: int, base_dir: str, session, cached_webfingers: {}, person_cache: {}, nickname: str, domain: str, port: int, post_json_object: {}, avatar_url: str, showAvatarOptions: bool, allow_deletion: bool, http_prefix: str, project_version: str, box_name: str, yt_replace_domain: str, twitter_replacement_domain: str, show_published_date_only: bool, peertube_instances: [], allow_local_network_access: bool, theme_name: str, system_language: str, max_like_count: int, show_repeats: bool, show_icons: bool, manually_approves_followers: bool, show_public_only: bool, store_to_cache: bool, use_cache_only: bool, cw_lists: {}, lists_enabled: str, timezone: str, mitm: bool, bold_reading: bool) -> str: """ Shows a single post as html """ if not post_json_object: return '' # maximum number of different emoji reactions which can be added to a post max_reaction_types = 5 # benchmark post_start_time = time.time() post_actor = post_json_object['actor'] # ZZZzzz if is_person_snoozed(base_dir, nickname, domain, post_actor): return '' # if downloads of avatar images aren't enabled then we can do more # accurate timing of different parts of the code enable_timing_log = not allow_downloads _log_post_timing(enable_timing_log, post_start_time, '1') avatar_position = '' message_id = '' if post_json_object.get('id'): message_id = remove_hash_from_post_id(post_json_object['id']) message_id = remove_id_ending(message_id) _log_post_timing(enable_timing_log, post_start_time, '2') message_id_str = '' if message_id: message_id_str = ';' + message_id domain_full = get_full_domain(domain, port) page_number_param = '' if page_number: page_number_param = '?page=' + str(page_number) # get the html post from the recent posts cache if it exists there post_html = \ _get_post_from_recent_cache(session, base_dir, http_prefix, nickname, domain, post_json_object, post_actor, person_cache, allow_downloads, show_public_only, store_to_cache, box_name, avatar_url, enable_timing_log, post_start_time, page_number, recent_posts_cache, max_recent_posts, signing_priv_key_pem) if post_html: return post_html if use_cache_only and post_json_object['type'] != 'Announce': return '' _log_post_timing(enable_timing_log, post_start_time, '4') avatar_url = \ get_avatar_image_url(session, base_dir, http_prefix, post_actor, person_cache, avatar_url, allow_downloads, signing_priv_key_pem) _log_post_timing(enable_timing_log, post_start_time, '5') # get the display name if domain_full not in post_actor: # lookup the correct webfinger for the post_actor post_actor_nickname = get_nickname_from_actor(post_actor) if not post_actor_nickname: return '' post_actor_domain, post_actor_port = get_domain_from_actor(post_actor) post_actor_domain_full = \ get_full_domain(post_actor_domain, post_actor_port) post_actor_handle = post_actor_nickname + '@' + post_actor_domain_full post_actor_wf = \ webfinger_handle(session, post_actor_handle, http_prefix, cached_webfingers, domain, __version__, False, False, signing_priv_key_pem) avatar_url2 = None display_name = None if post_actor_wf: origin_domain = domain (_, _, _, _, _, avatar_url2, display_name, _) = get_person_box(signing_priv_key_pem, origin_domain, base_dir, session, post_actor_wf, person_cache, project_version, http_prefix, nickname, domain, 'outbox', 72367) _log_post_timing(enable_timing_log, post_start_time, '6') if avatar_url2: avatar_url = avatar_url2 if display_name: # add any emoji to the display name if ':' in display_name: display_name = \ add_emoji_to_display_name(session, base_dir, http_prefix, nickname, domain, display_name, False) _log_post_timing(enable_timing_log, post_start_time, '7') avatar_link = \ _get_avatar_image_html(showAvatarOptions, nickname, domain_full, avatar_url, post_actor, translate, avatar_position, page_number, message_id_str) avatar_image_in_post = \ '
    ' + avatar_link + '
    \n' timeline_post_bookmark = remove_id_ending(post_json_object['id']) timeline_post_bookmark = timeline_post_bookmark.replace('://', '-') timeline_post_bookmark = timeline_post_bookmark.replace('/', '-') # If this is the inbox timeline then don't show the repeat icon on any DMs show_repeat_icon = show_repeats is_public_repeat = False post_is_dm = is_dm(post_json_object) if show_repeats: if post_is_dm: show_repeat_icon = False else: if not is_public_post(post_json_object): is_public_repeat = True title_str = '' gallery_str = '' is_announced = False announce_json_object = None if post_json_object['type'] == 'Announce': announce_json_object = post_json_object.copy() blocked_cache = {} post_json_announce = \ download_announce(session, base_dir, http_prefix, nickname, domain, post_json_object, project_version, translate, yt_replace_domain, twitter_replacement_domain, allow_local_network_access, recent_posts_cache, False, system_language, domain_full, person_cache, signing_priv_key_pem, blocked_cache, bold_reading) if not post_json_announce: # if the announce could not be downloaded then mark it as rejected announced_post_id = remove_id_ending(post_json_object['id']) reject_post_id(base_dir, nickname, domain, announced_post_id, recent_posts_cache) return '' post_json_object = post_json_announce # is the announced post in the html cache? post_html = \ _get_post_from_recent_cache(session, base_dir, http_prefix, nickname, domain, post_json_object, post_actor, person_cache, allow_downloads, show_public_only, store_to_cache, box_name, avatar_url, enable_timing_log, post_start_time, page_number, recent_posts_cache, max_recent_posts, signing_priv_key_pem) if post_html: return post_html announce_filename = \ locate_post(base_dir, nickname, domain, post_json_object['id']) if announce_filename: update_announce_collection(recent_posts_cache, base_dir, announce_filename, post_actor, nickname, domain_full, False) # create a file for use by text-to-speech if is_recent_post(post_json_object, 3): if post_json_object.get('actor'): if not os.path.isfile(announce_filename + '.tts'): update_speaker(base_dir, http_prefix, nickname, domain, domain_full, post_json_object, person_cache, translate, post_json_object['actor'], theme_name) with open(announce_filename + '.tts', 'w+') as ttsfile: ttsfile.write('\n') is_announced = True _log_post_timing(enable_timing_log, post_start_time, '8') if not has_object_dict(post_json_object): return '' # if this post should be public then check its recipients if show_public_only: if not post_contains_public(post_json_object): return '' is_moderation_post = False if post_json_object['object'].get('moderationStatus'): is_moderation_post = True container_class = 'container' container_class_icons = 'containericons' time_class = 'time-right' actor_nickname = get_nickname_from_actor(post_actor) if not actor_nickname: # single user instance actor_nickname = 'dev' actor_domain, _ = get_domain_from_actor(post_actor) display_name = get_display_name(base_dir, post_actor, person_cache) if display_name: if ':' in display_name: display_name = \ add_emoji_to_display_name(session, base_dir, http_prefix, nickname, domain, display_name, False) title_str += \ ' ' + display_name + '\n' else: if not message_id: # pprint(post_json_object) print('ERROR: no message_id') if not actor_nickname: # pprint(post_json_object) print('ERROR: no actor_nickname') if not actor_domain: # pprint(post_json_object) print('ERROR: no actor_domain') title_str += \ ' @' + actor_nickname + '@' + actor_domain + '\n' # benchmark 9 _log_post_timing(enable_timing_log, post_start_time, '9') # Show a DM icon for DMs in the inbox timeline if post_is_dm: title_str = \ title_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, domain_full, 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') attachment_str, gallery_str = \ get_post_attachments_as_html(post_json_object, box_name, translate, is_muted, avatar_link, reply_str, announce_str, like_str, bookmark_str, delete_str, mute_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) 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) person_url = local_actor_url(http_prefix, nickname, domain_full) actor_json = \ get_person_from_cache(base_dir, person_url, person_cache, False) 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) if not content_str: content_str = \ auto_translate_post(base_dir, post_json_object, system_language, translate) if not content_str: return '' # does an emoji indicate no boost preference? # if so then don't show the repeat/announce icon if content_str: if ':boost_no:' in content_str or \ ':noboost:' in content_str or \ ':noboosts:' in content_str or \ ':no_boost:' in content_str or \ ':no_boosts:' in content_str or \ ':boosts_no:' in content_str: announce_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 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 not is_patch: # 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) 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 = '
    ' + object_content + '
    ' if not post_is_sensitive: content_str = object_content + attachment_str content_str = add_embedded_elements(translate, content_str, peertube_instances) content_str = insert_question(base_dir, translate, nickname, domain, port, content_str, post_json_object, page_number) else: post_id = 'post' + str(create_password(8)) content_str = '' if summary_str: cw_str = \ add_emoji_to_display_name(session, base_dir, http_prefix, nickname, domain, summary_str, False) content_str += \ '\n ' if is_moderation_post: container_class = 'container report' # get the content warning text cw_content_str = object_content + attachment_str if not is_patch: cw_content_str = add_embedded_elements(translate, cw_content_str, peertube_instances) cw_content_str = \ insert_question(base_dir, translate, nickname, domain, port, cw_content_str, post_json_object, page_number) cw_content_str = \ switch_words(base_dir, nickname, domain, cw_content_str) if not is_blog_post(post_json_object): # get the content warning button content_str += \ get_content_warning_button(post_id, translate, cw_content_str) else: content_str += cw_content_str _log_post_timing(enable_timing_log, post_start_time, '17') if post_json_object['object'].get('tag') and not is_patch: content_str = \ replace_emoji_from_tags(session, base_dir, content_str, post_json_object['object']['tag'], 'content', False) if is_muted: content_str = '' else: if not is_patch: content_str = '
    ' + \ content_str + \ '
    \n' else: content_str = \ '
    ' + content_str + \
                    '
    \n' # show blog citations citations_str = \ _get_blog_citations_html(box_name, post_json_object, translate) post_html = '' if box_name != 'tlmedia': reaction_str = '' if show_icons: reaction_str = \ html_emoji_reactions(post_json_object, True, person_url, max_reaction_types, box_name, page_number) if post_is_sensitive and reaction_str: reaction_str = '
    ' + reaction_str post_html = '
    \n' post_html += avatar_image_in_post post_html += '
    \n' + \ ' ' + title_str + \ reply_avatar_image_in_post + '
    \n' post_html += \ content_str + citations_str + reaction_str + footer_str + '\n' post_html += '
    \n' else: post_html = gallery_str _log_post_timing(enable_timing_log, post_start_time, '18') # save the created html to the recent posts cache if not show_public_only and store_to_cache and \ box_name != 'tlmedia' and box_name != 'tlbookmarks' and \ box_name != 'bookmarks': _save_individual_post_as_html_to_cache(base_dir, nickname, domain, post_json_object, post_html) update_recent_posts_cache(recent_posts_cache, max_recent_posts, post_json_object, post_html) _log_post_timing(enable_timing_log, post_start_time, '19') return post_html def html_individual_post(css_cache: {}, recent_posts_cache: {}, max_recent_posts: int, translate: {}, base_dir: str, session, cached_webfingers: {}, person_cache: {}, nickname: str, domain: str, port: int, authorized: bool, post_json_object: {}, http_prefix: str, project_version: str, likedBy: str, reactBy: str, reactEmoji: str, yt_replace_domain: str, twitter_replacement_domain: str, show_published_date_only: bool, peertube_instances: [], allow_local_network_access: bool, theme_name: str, system_language: str, max_like_count: int, signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, timezone: str, mitm: bool, bold_reading: bool) -> str: """Show an individual post as html """ original_post_json = post_json_object post_str = '' by_str = '' by_text = '' by_text_extra = '' if likedBy: by_str = likedBy by_text = 'Liked by' elif reactBy and reactEmoji: by_str = reactBy by_text = 'Reaction by' by_text_extra = ' ' + reactEmoji if by_str: by_str_nickname = get_nickname_from_actor(by_str) if not by_str_nickname: return '' by_str_domain, by_str_port = get_domain_from_actor(by_str) by_str_domain = get_full_domain(by_str_domain, by_str_port) by_str_handle = by_str_nickname + '@' + by_str_domain if translate.get(by_text): by_text = translate[by_text] post_str += \ '

    ' + by_text + ' @' + \ by_str_handle + '' + by_text_extra + '\n' domain_full = get_full_domain(domain, port) actor = '/users/' + nickname follow_str = '

    \n' follow_str += \ ' \n' follow_str += \ ' \n' if not is_following_actor(base_dir, nickname, domain_full, by_str): translate_follow_str = 'Follow' if translate.get(translate_follow_str): translate_follow_str = translate[translate_follow_str] follow_str += ' \n' go_back_str = 'Go Back' if translate.get(go_back_str): go_back_str = translate[go_back_str] follow_str += ' \n' follow_str += '
    \n' post_str += follow_str + '

    \n' post_str += \ individual_post_as_html(signing_priv_key_pem, True, recent_posts_cache, max_recent_posts, translate, None, base_dir, session, cached_webfingers, person_cache, nickname, domain, port, post_json_object, None, True, False, http_prefix, project_version, 'inbox', yt_replace_domain, twitter_replacement_domain, show_published_date_only, peertube_instances, allow_local_network_access, theme_name, system_language, max_like_count, False, authorized, False, False, False, False, cw_lists, lists_enabled, timezone, mitm, bold_reading) message_id = remove_id_ending(post_json_object['id']) # show the previous posts if has_object_dict(post_json_object): while post_json_object['object'].get('inReplyTo'): post_filename = \ locate_post(base_dir, nickname, domain, post_json_object['object']['inReplyTo']) if not post_filename: break post_json_object = load_json(post_filename) if post_json_object: mitm = False if os.path.isfile(post_filename.replace('.json', '') + '.mitm'): mitm = True post_str = \ individual_post_as_html(signing_priv_key_pem, True, recent_posts_cache, max_recent_posts, translate, None, base_dir, session, cached_webfingers, person_cache, nickname, domain, port, post_json_object, None, True, False, http_prefix, project_version, 'inbox', yt_replace_domain, twitter_replacement_domain, show_published_date_only, peertube_instances, allow_local_network_access, theme_name, system_language, max_like_count, False, authorized, False, False, False, False, cw_lists, lists_enabled, timezone, mitm, bold_reading) + post_str # show the following posts post_filename = locate_post(base_dir, nickname, domain, message_id) if post_filename: # is there a replies file for this post? replies_filename = post_filename.replace('.json', '.replies') if os.path.isfile(replies_filename): # get items from the replies file replies_json = { 'orderedItems': [] } populate_replies_json(base_dir, nickname, domain, replies_filename, authorized, replies_json) # add items to the html output for item in replies_json['orderedItems']: post_str += \ individual_post_as_html(signing_priv_key_pem, True, recent_posts_cache, max_recent_posts, translate, None, base_dir, session, cached_webfingers, person_cache, nickname, domain, port, item, None, True, False, http_prefix, project_version, 'inbox', yt_replace_domain, twitter_replacement_domain, show_published_date_only, peertube_instances, allow_local_network_access, theme_name, system_language, max_like_count, False, authorized, False, False, False, False, cw_lists, lists_enabled, timezone, False, bold_reading) css_filename = base_dir + '/epicyon-profile.css' if os.path.isfile(base_dir + '/epicyon.css'): css_filename = base_dir + '/epicyon.css' instance_title = \ get_config_param(base_dir, 'instanceTitle') metadata_str = _html_post_metadata_open_graph(domain, original_post_json) header_str = html_header_with_external_style(css_filename, instance_title, metadata_str) return header_str + post_str + html_footer() def html_post_replies(css_cache: {}, recent_posts_cache: {}, max_recent_posts: int, translate: {}, base_dir: str, session, cached_webfingers: {}, person_cache: {}, nickname: str, domain: str, port: int, replies_json: {}, http_prefix: str, project_version: str, yt_replace_domain: str, twitter_replacement_domain: str, show_published_date_only: bool, peertube_instances: [], allow_local_network_access: bool, theme_name: str, system_language: str, max_like_count: int, signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, timezone: str, bold_reading: bool) -> str: """Show the replies to an individual post as html """ replies_str = '' if replies_json.get('orderedItems'): for item in replies_json['orderedItems']: replies_str += \ individual_post_as_html(signing_priv_key_pem, True, recent_posts_cache, max_recent_posts, translate, None, base_dir, session, cached_webfingers, person_cache, nickname, domain, port, item, None, True, False, http_prefix, project_version, 'inbox', yt_replace_domain, twitter_replacement_domain, show_published_date_only, peertube_instances, allow_local_network_access, theme_name, system_language, max_like_count, False, False, False, False, False, False, cw_lists, lists_enabled, timezone, False, bold_reading) css_filename = base_dir + '/epicyon-profile.css' if os.path.isfile(base_dir + '/epicyon.css'): css_filename = base_dir + '/epicyon.css' instance_title = get_config_param(base_dir, 'instanceTitle') metadata = '' header_str = \ html_header_with_external_style(css_filename, instance_title, metadata) return header_str + replies_str + html_footer() def html_emoji_reaction_picker(css_cache: {}, recent_posts_cache: {}, max_recent_posts: int, translate: {}, base_dir: str, session, cached_webfingers: {}, person_cache: {}, nickname: str, domain: str, port: int, post_json_object: {}, http_prefix: str, project_version: str, yt_replace_domain: str, twitter_replacement_domain: str, show_published_date_only: bool, peertube_instances: [], allow_local_network_access: bool, theme_name: str, system_language: str, max_like_count: int, signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, box_name: str, page_number: int, timezone: str, bold_reading: bool) -> str: """Returns the emoji picker screen """ reacted_to_post_str = \ '
    \n' + \ individual_post_as_html(signing_priv_key_pem, True, recent_posts_cache, max_recent_posts, translate, None, base_dir, session, cached_webfingers, person_cache, nickname, domain, port, post_json_object, None, True, False, http_prefix, project_version, 'inbox', yt_replace_domain, twitter_replacement_domain, show_published_date_only, peertube_instances, allow_local_network_access, theme_name, system_language, max_like_count, False, False, False, False, False, False, cw_lists, lists_enabled, timezone, False, bold_reading) reactions_filename = base_dir + '/emoji/reactions.json' if not os.path.isfile(reactions_filename): reactions_filename = base_dir + '/emoji/default_reactions.json' reactions_json = load_json(reactions_filename) emoji_picks_str = '' base_url = '/users/' + nickname post_id = remove_id_ending(post_json_object['id']) for _, item in reactions_json.items(): emoji_picks_str += '
    \n' for emoji_content in item: emoji_content_encoded = urllib.parse.quote_plus(emoji_content) emoji_url = \ base_url + '?react=' + post_id + \ '?actor=' + post_json_object['actor'] + \ '?tl=' + box_name + \ '?page=' + str(page_number) + \ '?emojreact=' + emoji_content_encoded emoji_label = '' emoji_picks_str += \ ' ' + emoji_label + '\n' emoji_picks_str += '
    \n' css_filename = base_dir + '/epicyon-profile.css' if os.path.isfile(base_dir + '/epicyon.css'): css_filename = base_dir + '/epicyon.css' # filename of the banner shown at the top banner_file, _ = \ get_banner_file(base_dir, nickname, domain, theme_name) instance_title = get_config_param(base_dir, 'instanceTitle') metadata = '' header_str = \ html_header_with_external_style(css_filename, instance_title, metadata) # banner header_str += \ '
    \n' + \ '\n' header_str += '\n' + \ '
    \n' return header_str + reacted_to_post_str + emoji_picks_str + html_footer()