__filename__ = "webapp_post.py" __author__ = "Bob Mottram" __license__ = "AGPL3+" __version__ = "1.2.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 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_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 has_object_dict from utils import update_announce_collection from utils import is_pgp_encrypted from utils import is_dm 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 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 announce import announced_by_person 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" objJson = post_json_object if has_object_dict(post_json_object): objJson = post_json_object['object'] if objJson.get('attributedTo'): if isinstance(objJson['attributedTo'], str): attrib = objJson['attributedTo'] actorNick = get_nickname_from_actor(attrib) actorDomain, _ = get_domain_from_actor(attrib) actorHandle = actorNick + '@' + actorDomain metadata += \ " \n" if objJson.get('url'): metadata += \ " \n" if objJson.get('published'): metadata += \ " \n" if not objJson.get('attachment') or objJson.get('sensitive'): if objJson.get('content') and not objJson.get('sensitive'): description = remove_html(objJson['content']) metadata += \ " \n" metadata += \ " \n" return metadata # metadata for attachment for attachJson in objJson['attachment']: if not isinstance(attachJson, dict): continue if not attachJson.get('mediaType'): continue if not attachJson.get('url'): continue if not attachJson.get('name'): continue description = None if attachJson['mediaType'].startswith('image/'): description = 'Attached: 1 image' elif attachJson['mediaType'].startswith('video/'): description = 'Attached: 1 video' elif attachJson['mediaType'].startswith('audio/'): description = 'Attached: 1 audio' if description: if objJson.get('content') and not objJson.get('sensitive'): description += '\n\n' + remove_html(objJson['content']) metadata += \ " \n" metadata += \ " \n" metadata += \ " \n" metadata += \ " \n" if attachJson.get('width'): metadata += \ " \n" if attachJson.get('height'): metadata += \ " \n" metadata += \ " \n" if attachJson['mediaType'].startswith('image/'): metadata += \ " \n" return metadata def _log_post_timing(enableTimingLog: bool, postStartTime, debugId: str) -> None: """Create a log of timings for performance tuning """ if not enableTimingLog: return timeDiff = int((time.time() - postStartTime) * 1000) if timeDiff > 100: print('TIMING INDIV ' + debugId + ' = ' + str(timeDiff)) def prepare_html_post_nickname(nickname: str, postHtml: 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 usersStr = ' href="/users/' if usersStr not in postHtml: return postHtml userFound = True postStr = postHtml newPostStr = '' while userFound: if usersStr not in postStr: newPostStr += postStr break # the next part, after href="/users/nickname? nextStr = postStr.split(usersStr, 1)[1] if '?' in nextStr: nextStr = nextStr.split('?', 1)[1] else: newPostStr += postStr break # append the previous text to the result newPostStr += postStr.split(usersStr)[0] newPostStr += usersStr + nickname + '?' # post is now the next part postStr = nextStr return newPostStr def prepare_post_from_html_cache(nickname: str, postHtml: str, boxName: str, pageNumber: int) -> str: """Sets the page number on a cached html post """ # if on the bookmarks timeline then remain there if boxName == 'tlbookmarks' or boxName == 'bookmarks': postHtml = postHtml.replace('?tl=inbox', '?tl=tlbookmarks') if '?page=' in postHtml: pageNumberStr = postHtml.split('?page=')[1] if '?' in pageNumberStr: pageNumberStr = pageNumberStr.split('?')[0] postHtml = postHtml.replace('?page=' + pageNumberStr, '?page=-999') withPageNumber = postHtml.replace(';-999;', ';' + str(pageNumber) + ';') withPageNumber = withPageNumber.replace('?page=-999', '?page=' + str(pageNumber)) return prepare_html_post_nickname(nickname, withPageNumber) def _save_individual_post_as_html_to_cache(base_dir: str, nickname: str, domain: str, post_json_object: {}, postHtml: 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 """ htmlPostCacheDir = \ get_cached_post_directory(base_dir, nickname, domain) cachedPostFilename = \ get_cached_post_filename(base_dir, nickname, domain, post_json_object) # create the cache directory if needed if not os.path.isdir(htmlPostCacheDir): os.mkdir(htmlPostCacheDir) try: with open(cachedPostFilename, 'w+') as fp: fp.write(postHtml) 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: {}, postActor: str, person_cache: {}, allowDownloads: bool, showPublicOnly: bool, storeToCache: bool, boxName: str, avatarUrl: str, enableTimingLog: bool, postStartTime, pageNumber: 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 boxName == 'tlmedia': return None if showPublicOnly: return None tryCache = False bmTimeline = boxName == 'bookmarks' or boxName == 'tlbookmarks' if storeToCache or bmTimeline: tryCache = True if not tryCache: return None # update avatar if needed if not avatarUrl: avatarUrl = \ get_person_avatar_url(base_dir, postActor, person_cache, allowDownloads) _log_post_timing(enableTimingLog, postStartTime, '2.1') update_avatar_image_cache(signing_priv_key_pem, session, base_dir, http_prefix, postActor, avatarUrl, person_cache, allowDownloads) _log_post_timing(enableTimingLog, postStartTime, '2.2') postHtml = \ load_individual_post_as_html_from_cache(base_dir, nickname, domain, post_json_object) if not postHtml: return None postHtml = \ prepare_post_from_html_cache(nickname, postHtml, boxName, pageNumber) update_recent_posts_cache(recent_posts_cache, max_recent_posts, post_json_object, postHtml) _log_post_timing(enableTimingLog, postStartTime, '3') return postHtml def _get_avatar_image_html(showAvatarOptions: bool, nickname: str, domain_full: str, avatarUrl: str, postActor: str, translate: {}, avatarPosition: str, pageNumber: int, messageIdStr: str) -> str: """Get html for the avatar image """ avatarLink = '' if '/users/news/' not in avatarUrl: avatarLink = ' ' showProfileStr = 'Show profile' if translate.get(showProfileStr): showProfileStr = translate[showProfileStr] avatarLink += \ ' \n' if showAvatarOptions and \ domain_full + '/users/' + nickname not in postActor: showOptionsForThisPersonStr = 'Show options for this person' if translate.get(showOptionsForThisPersonStr): showOptionsForThisPersonStr = \ translate[showOptionsForThisPersonStr] if '/users/news/' not in avatarUrl: avatarLink = \ ' \n' avatarLink += \ ' \n' else: # don't link to the person options for the news account avatarLink += \ ' \n' return avatarLink.strip() def _get_reply_icon_html(base_dir: str, nickname: str, domain: str, isPublicRepeat: bool, showIcons: bool, commentsEnabled: bool, post_json_object: {}, pageNumberParam: str, translate: {}, system_language: str, conversationId: str) -> str: """Returns html for the reply icon/button """ replyStr = '' if not (showIcons and commentsEnabled): return replyStr # reply is permitted - create reply icon replyToLink = remove_hash_from_post_id(post_json_object['object']['id']) replyToLink = remove_id_ending(replyToLink) # see Mike MacGirvin's replyTo suggestion if post_json_object['object'].get('replyTo'): # check that the alternative replyTo url is not blocked blockNickname = \ get_nickname_from_actor(post_json_object['object']['replyTo']) blockDomain, _ = \ get_domain_from_actor(post_json_object['object']['replyTo']) if not is_blocked(base_dir, nickname, domain, blockNickname, blockDomain, {}): replyToLink = post_json_object['object']['replyTo'] if post_json_object['object'].get('attributedTo'): if isinstance(post_json_object['object']['attributedTo'], str): replyToLink += \ '?mention=' + post_json_object['object']['attributedTo'] content = get_base_content_from_post(post_json_object, system_language) if content: mentionedActors = \ get_mentions_from_html(content, "\n' else: if is_dm(post_json_object): replyStr += \ ' ' + \ '\n' else: replyStr += \ ' ' + \ '\n' replyStr += \ ' ' + \ '' + replyToThisPostStr + \
        ' |\n' return replyStr def _get_edit_icon_html(base_dir: str, nickname: str, domain_full: str, post_json_object: {}, actorNickname: str, translate: {}, isEvent: bool) -> str: """Returns html for the edit icon/button """ editStr = '' 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 editStr if is_blog_post(post_json_object): editBlogPostStr = 'Edit blog post' if translate.get(editBlogPostStr): editBlogPostStr = translate[editBlogPostStr] if not is_news_post(post_json_object): editStr += \ ' ' + \ '' + \ '' + editBlogPostStr + \
                    ' |\n' else: editStr += \ ' ' + \ '' + \ '' + editBlogPostStr + \
                    ' |\n' elif isEvent: editEventStr = 'Edit event' if translate.get(editEventStr): editEventStr = translate[editEventStr] editStr += \ ' ' + \ '' + \ '' + editEventStr + \
                ' |\n' return editStr def _get_announce_icon_html(isAnnounced: bool, postActor: str, nickname: str, domain_full: str, announceJsonObject: {}, post_json_object: {}, isPublicRepeat: bool, isModerationPost: bool, showRepeatIcon: bool, translate: {}, pageNumberParam: str, timelinePostBookmark: str, boxName: str) -> str: """Returns html for announce icon/button """ announceStr = '' if not showRepeatIcon: return announceStr if isModerationPost: return announceStr # don't allow announce/repeat of your own posts announceIcon = 'repeat_inactive.png' announceLink = 'repeat' announceEmoji = '' if not isPublicRepeat: announceLink = 'repeatprivate' repeatThisPostStr = 'Repeat this post' if translate.get(repeatThisPostStr): repeatThisPostStr = translate[repeatThisPostStr] announceTitle = repeatThisPostStr unannounceLinkStr = '' if announced_by_person(isAnnounced, postActor, nickname, domain_full): announceIcon = 'repeat.png' announceEmoji = '🔁 ' announceLink = 'unrepeat' if not isPublicRepeat: announceLink = 'unrepeatprivate' undoTheRepeatStr = 'Undo the repeat' if translate.get(undoTheRepeatStr): undoTheRepeatStr = translate[undoTheRepeatStr] announceTitle = undoTheRepeatStr if announceJsonObject: unannounceLinkStr = '?unannounce=' + \ remove_id_ending(announceJsonObject['id']) announcePostId = remove_hash_from_post_id(post_json_object['object']['id']) announcePostId = remove_id_ending(announcePostId) announceLinkStr = '?' + \ announceLink + '=' + announcePostId + pageNumberParam announceStr = \ ' \n' announceStr += \ ' ' + \ '' + announceEmoji + announceTitle + \
        ' |\n' return announceStr def _get_like_icon_html(nickname: str, domain_full: str, isModerationPost: bool, showLikeButton: bool, post_json_object: {}, enableTimingLog: bool, postStartTime, translate: {}, pageNumberParam: str, timelinePostBookmark: str, boxName: str, max_like_count: int) -> str: """Returns html for like icon/button """ if not showLikeButton or isModerationPost: return '' likeStr = '' likeIcon = 'like_inactive.png' likeLink = 'like' likeTitle = 'Like this post' if translate.get(likeTitle): likeTitle = translate[likeTitle] likeEmoji = '' likeCount = no_of_likes(post_json_object) _log_post_timing(enableTimingLog, postStartTime, '12.1') likeCountStr = '' if likeCount > 0: if likeCount <= max_like_count: likeCountStr = ' (' + str(likeCount) + ')' else: likeCountStr = ' (' + str(max_like_count) + '+)' if liked_by_person(post_json_object, nickname, domain_full): if likeCount == 1: # liked by the reader only likeCountStr = '' likeIcon = 'like.png' likeLink = 'unlike' likeTitle = 'Undo the like' if translate.get(likeTitle): likeTitle = translate[likeTitle] likeEmoji = '👍 ' _log_post_timing(enableTimingLog, postStartTime, '12.2') likeStr = '' if likeCountStr: # show the number of likes next to icon likeStr += '\n' like_postId = remove_hash_from_post_id(post_json_object['id']) like_postId = remove_id_ending(like_postId) likeStr += \ ' \n' likeStr += \ ' ' + \ '' + likeEmoji + likeTitle + \
        ' |\n' return likeStr def _get_bookmark_icon_html(nickname: str, domain_full: str, post_json_object: {}, isModerationPost: bool, translate: {}, enableTimingLog: bool, postStartTime, boxName: str, pageNumberParam: str, timelinePostBookmark: str) -> str: """Returns html for bookmark icon/button """ bookmarkStr = '' if isModerationPost: return bookmarkStr bookmarkIcon = 'bookmark_inactive.png' bookmarkLink = 'bookmark' bookmarkEmoji = '' bookmarkTitle = 'Bookmark this post' if translate.get(bookmarkTitle): bookmarkTitle = translate[bookmarkTitle] if bookmarked_by_person(post_json_object, nickname, domain_full): bookmarkIcon = 'bookmark.png' bookmarkLink = 'unbookmark' bookmarkEmoji = '🔖 ' bookmarkTitle = 'Undo the bookmark' if translate.get(bookmarkTitle): bookmarkTitle = translate[bookmarkTitle] _log_post_timing(enableTimingLog, postStartTime, '12.6') bookmarkPostId = remove_hash_from_post_id(post_json_object['object']['id']) bookmarkPostId = remove_id_ending(bookmarkPostId) bookmarkStr = \ ' \n' bookmarkStr += \ ' ' + \ '' + \
        bookmarkEmoji + bookmarkTitle + ' |\n' return bookmarkStr def _get_reaction_icon_html(nickname: str, domain_full: str, post_json_object: {}, isModerationPost: bool, showReactionButton: bool, translate: {}, enableTimingLog: bool, postStartTime, boxName: str, pageNumberParam: str, timelinePostReaction: str) -> str: """Returns html for reaction icon/button """ reactionStr = '' if not showReactionButton or isModerationPost: return reactionStr reactionIcon = 'reaction.png' reactionTitle = 'Select reaction' if translate.get(reactionTitle): reactionTitle = translate[reactionTitle] _log_post_timing(enableTimingLog, postStartTime, '12.65') reaction_postId = \ remove_hash_from_post_id(post_json_object['object']['id']) reaction_postId = remove_id_ending(reaction_postId) reactionStr = \ ' \n' reactionStr += \ ' ' + \ '' + \
        reactionTitle + ' |\n' return reactionStr def _get_mute_icon_html(is_muted: bool, postActor: str, messageId: str, nickname: str, domain_full: str, allow_deletion: bool, pageNumberParam: str, boxName: str, timelinePostBookmark: str, translate: {}) -> str: """Returns html for mute icon/button """ muteStr = '' if (allow_deletion or ('/' + domain_full + '/' in postActor and messageId.startswith(postActor))): return muteStr if not is_muted: muteThisPostStr = 'Mute this post' if translate.get('Mute this post'): muteThisPostStr = translate[muteThisPostStr] muteStr = \ ' \n' muteStr += \ ' ' + \ '' + \
            muteThisPostStr + \
            ' |\n' else: undoMuteStr = 'Undo mute' if translate.get(undoMuteStr): undoMuteStr = translate[undoMuteStr] muteStr = \ ' \n' muteStr += \ ' ' + \ '🔇 ' + undoMuteStr + \
            ' |\n' return muteStr def _get_delete_icon_html(nickname: str, domain_full: str, allow_deletion: bool, postActor: str, messageId: str, post_json_object: {}, pageNumberParam: str, translate: {}) -> str: """Returns html for delete icon/button """ deleteStr = '' if (allow_deletion or ('/' + domain_full + '/' in postActor and messageId.startswith(postActor))): if '/users/' + nickname + '/' in messageId: if not is_news_post(post_json_object): deleteThisPostStr = 'Delete this post' if translate.get(deleteThisPostStr): deleteThisPostStr = translate[deleteThisPostStr] deleteStr = \ ' \n' deleteStr += \ ' ' + \ '' + \
                    deleteThisPostStr + \
                    ' |\n' return deleteStr def _get_published_date_str(post_json_object: {}, show_published_date_only: bool) -> str: """Return the html for the published date on a post """ publishedStr = '' if not post_json_object['object'].get('published'): return publishedStr publishedStr = post_json_object['object']['published'] if '.' not in publishedStr: if '+' not in publishedStr: datetimeObject = \ datetime.strptime(publishedStr, "%Y-%m-%dT%H:%M:%SZ") else: datetimeObject = \ datetime.strptime(publishedStr.split('+')[0] + 'Z', "%Y-%m-%dT%H:%M:%SZ") else: publishedStr = \ publishedStr.replace('T', ' ').split('.')[0] datetimeObject = parse(publishedStr) if not show_published_date_only: publishedStr = datetimeObject.strftime("%a %b %d, %H:%M") else: publishedStr = datetimeObject.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: publishedStr = '[' + publishedStr + ']' return publishedStr def _get_blog_citations_html(boxName: str, post_json_object: {}, translate: {}) -> str: """Returns blog citations as html """ # show blog citations citationsStr = '' if not (boxName == 'tlblogs' or boxName == 'tlfeatures'): return citationsStr if not post_json_object['object'].get('tag'): return citationsStr for tagJson in post_json_object['object']['tag']: if not isinstance(tagJson, dict): continue if not tagJson.get('type'): continue if tagJson['type'] != 'Article': continue if not tagJson.get('name'): continue if not tagJson.get('url'): continue citationsStr += \ '
  • ' + \ '' + tagJson['name'] + '
  • \n' if citationsStr: translatedCitationsStr = 'Citations' if translate.get(translatedCitationsStr): translatedCitationsStr = translate[translatedCitationsStr] citationsStr = '

    ' + translatedCitationsStr + ':

    ' + \ '\n' return citationsStr def _boost_own_post_html(translate: {}) -> str: """The html title for announcing your own post """ announcesStr = 'announces' if translate.get(announcesStr): announcesStr = translate[announcesStr] return ' ' + announcesStr + \
        '\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 """ announcesStr = 'announces' if translate.get(announcesStr): announcesStr = translate[announcesStr] post_id = remove_id_ending(post_json_object['object']['id']) return ' ' + \
        announcesStr + '\n' + \ ' @unattributed\n' def _announce_with_display_name_html(translate: {}, post_json_object: {}, announceDisplayName: str) -> str: """Returns html for an announce having a display name """ announcesStr = 'announces' if translate.get(announcesStr): announcesStr = translate[announcesStr] post_id = remove_id_ending(post_json_object['object']['id']) return ' ' + \
        announcesStr + '\n' + \ ' ' + announceDisplayName + '\n' def _get_post_title_announce_html(base_dir: str, http_prefix: str, nickname: str, domain: str, showRepeatIcon: bool, isAnnounced: bool, post_json_object: {}, postActor: str, translate: {}, enableTimingLog: bool, postStartTime, boxName: str, person_cache: {}, allowDownloads: bool, avatarPosition: str, pageNumber: int, messageIdStr: str, containerClassIcons: str, containerClass: str) -> (str, str, str, str): """Returns the announce title of a post containing names of participants x announces y """ titleStr = '' replyAvatarImageInPost = '' objJson = post_json_object['object'] # has no attribution if not objJson.get('attributedTo'): titleStr += _announce_unattributed_html(translate, post_json_object) return (titleStr, replyAvatarImageInPost, containerClassIcons, containerClass) attributedTo = '' if isinstance(objJson['attributedTo'], str): attributedTo = objJson['attributedTo'] # boosting your own post if attributedTo.startswith(postActor): titleStr += _boost_own_post_html(translate) return (titleStr, replyAvatarImageInPost, containerClassIcons, containerClass) # boosting another person's post _log_post_timing(enableTimingLog, postStartTime, '13.2') announceNickname = None if attributedTo: announceNickname = get_nickname_from_actor(attributedTo) if not announceNickname: titleStr += _announce_unattributed_html(translate, post_json_object) return (titleStr, replyAvatarImageInPost, containerClassIcons, containerClass) announceDomain, announcePort = get_domain_from_actor(attributedTo) get_person_from_cache(base_dir, attributedTo, person_cache, allowDownloads) announceDisplayName = \ get_display_name(base_dir, attributedTo, person_cache) if not announceDisplayName: announceDisplayName = announceNickname + '@' + announceDomain _log_post_timing(enableTimingLog, postStartTime, '13.3') # add any emoji to the display name if ':' in announceDisplayName: announceDisplayName = \ add_emoji_to_display_name(None, base_dir, http_prefix, nickname, domain, announceDisplayName, False) _log_post_timing(enableTimingLog, postStartTime, '13.3.1') titleStr += \ _announce_with_display_name_html(translate, post_json_object, announceDisplayName) # show avatar of person replied to announceActor = attributedTo announceAvatarUrl = \ get_person_avatar_url(base_dir, announceActor, person_cache, allowDownloads) _log_post_timing(enableTimingLog, postStartTime, '13.4') if not announceAvatarUrl: announceAvatarUrl = '' idx = 'Show options for this person' if '/users/news/' not in announceAvatarUrl: showOptionsForThisPersonStr = idx if translate.get(idx): showOptionsForThisPersonStr = translate[idx] replyAvatarImageInPost = \ '
    \n' \ ' ' \ ' \n
    \n' return (titleStr, replyAvatarImageInPost, containerClassIcons, containerClass) def _reply_to_yourself_html(translate: {}) -> str: """Returns html for a title which is a reply to yourself """ replyingToThemselvesStr = 'replying to themselves' if translate.get(replyingToThemselvesStr): replyingToThemselvesStr = translate[replyingToThemselvesStr] return ' ' + replyingToThemselvesStr + \
        '\n' def _reply_to_unknown_html(translate: {}, post_json_object: {}) -> str: """Returns the html title for a reply to an unknown handle """ replyingToStr = 'replying to' if translate.get(replyingToStr): replyingToStr = translate[replyingToStr] return ' ' + \
        replyingToStr + '\n' + \ ' @unknown\n' def _reply_with_unknown_path_html(translate: {}, post_json_object: {}, postDomain: str) -> str: """Returns html title for a reply with an unknown path eg. does not contain /statuses/ """ replyingToStr = 'replying to' if translate.get(replyingToStr): replyingToStr = translate[replyingToStr] return ' ' + replyingToStr + \
        '\n' + \ ' ' + \ postDomain + '\n' def _get_reply_html(translate: {}, inReplyTo: str, replyDisplayName: str) -> str: """Returns html title for a reply """ replyingToStr = 'replying to' if translate.get(replyingToStr): replyingToStr = translate[replyingToStr] return ' ' + \ '' + \
        replyingToStr + '\n' + \ ' ' + \ replyDisplayName + '\n' def _get_post_title_reply_html(base_dir: str, http_prefix: str, nickname: str, domain: str, showRepeatIcon: bool, isAnnounced: bool, post_json_object: {}, postActor: str, translate: {}, enableTimingLog: bool, postStartTime, boxName: str, person_cache: {}, allowDownloads: bool, avatarPosition: str, pageNumber: int, messageIdStr: str, containerClassIcons: str, containerClass: str) -> (str, str, str, str): """Returns the reply title of a post containing names of participants x replies to y """ titleStr = '' replyAvatarImageInPost = '' objJson = post_json_object['object'] # not a reply if not objJson.get('inReplyTo'): return (titleStr, replyAvatarImageInPost, containerClassIcons, containerClass) containerClassIcons = 'containericons darker' containerClass = 'container darker' # reply to self if objJson['inReplyTo'].startswith(postActor): titleStr += _reply_to_yourself_html(translate) return (titleStr, replyAvatarImageInPost, containerClassIcons, containerClass) # has a reply if '/statuses/' not in objJson['inReplyTo']: postDomain = objJson['inReplyTo'] prefixes = get_protocol_prefixes() for prefix in prefixes: postDomain = postDomain.replace(prefix, '') if '/' in postDomain: postDomain = postDomain.split('/', 1)[0] if postDomain: titleStr += \ _reply_with_unknown_path_html(translate, post_json_object, postDomain) return (titleStr, replyAvatarImageInPost, containerClassIcons, containerClass) inReplyTo = objJson['inReplyTo'] replyActor = inReplyTo.split('/statuses/')[0] replyNickname = get_nickname_from_actor(replyActor) if not replyNickname: titleStr += _reply_to_unknown_html(translate, post_json_object) return (titleStr, replyAvatarImageInPost, containerClassIcons, containerClass) replyDomain, replyPort = get_domain_from_actor(replyActor) if not (replyNickname and replyDomain): titleStr += _reply_to_unknown_html(translate, post_json_object) return (titleStr, replyAvatarImageInPost, containerClassIcons, containerClass) get_person_from_cache(base_dir, replyActor, person_cache, allowDownloads) replyDisplayName = get_display_name(base_dir, replyActor, person_cache) if not replyDisplayName: replyDisplayName = replyNickname + '@' + replyDomain # add emoji to the display name if ':' in replyDisplayName: _log_post_timing(enableTimingLog, postStartTime, '13.5') replyDisplayName = \ add_emoji_to_display_name(None, base_dir, http_prefix, nickname, domain, replyDisplayName, False) _log_post_timing(enableTimingLog, postStartTime, '13.6') titleStr += _get_reply_html(translate, inReplyTo, replyDisplayName) _log_post_timing(enableTimingLog, postStartTime, '13.7') # show avatar of person replied to replyAvatarUrl = \ get_person_avatar_url(base_dir, replyActor, person_cache, allowDownloads) _log_post_timing(enableTimingLog, postStartTime, '13.8') if replyAvatarUrl: showProfileStr = 'Show profile' if translate.get(showProfileStr): showProfileStr = translate[showProfileStr] replyAvatarImageInPost = \ '
    \n' + \ ' \n' + \ '  \n
    \n' return (titleStr, replyAvatarImageInPost, containerClassIcons, containerClass) def _get_post_title_html(base_dir: str, http_prefix: str, nickname: str, domain: str, showRepeatIcon: bool, isAnnounced: bool, post_json_object: {}, postActor: str, translate: {}, enableTimingLog: bool, postStartTime, boxName: str, person_cache: {}, allowDownloads: bool, avatarPosition: str, pageNumber: int, messageIdStr: str, containerClassIcons: str, containerClass: str) -> (str, str, str, str): """Returns the title of a post containing names of participants x replies to y, x announces y, etc """ if not isAnnounced and boxName == 'search' and \ post_json_object.get('object'): if post_json_object['object'].get('attributedTo'): if post_json_object['object']['attributedTo'] != postActor: isAnnounced = True if isAnnounced: return _get_post_title_announce_html(base_dir, http_prefix, nickname, domain, showRepeatIcon, isAnnounced, post_json_object, postActor, translate, enableTimingLog, postStartTime, boxName, person_cache, allowDownloads, avatarPosition, pageNumber, messageIdStr, containerClassIcons, containerClass) return _get_post_title_reply_html(base_dir, http_prefix, nickname, domain, showRepeatIcon, isAnnounced, post_json_object, postActor, translate, enableTimingLog, postStartTime, boxName, person_cache, allowDownloads, avatarPosition, pageNumber, messageIdStr, containerClassIcons, containerClass) def _get_footer_with_icons(showIcons: bool, containerClassIcons: str, replyStr: str, announceStr: str, likeStr: str, reactionStr: str, bookmarkStr: str, deleteStr: str, muteStr: str, editStr: str, post_json_object: {}, publishedLink: str, timeClass: str, publishedStr: str) -> str: """Returns the html for a post footer containing icons """ if not showIcons: return None footerStr = '\n \n' return footerStr def individual_post_as_html(signing_priv_key_pem: str, allowDownloads: bool, recent_posts_cache: {}, max_recent_posts: int, translate: {}, pageNumber: int, base_dir: str, session, cached_webfingers: {}, person_cache: {}, nickname: str, domain: str, port: int, post_json_object: {}, avatarUrl: str, showAvatarOptions: bool, allow_deletion: bool, http_prefix: str, project_version: str, boxName: 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, showRepeats: bool, showIcons: bool, manuallyApprovesFollowers: bool, showPublicOnly: bool, storeToCache: bool, useCacheOnly: bool, cw_lists: {}, lists_enabled: str) -> 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 maxReactionTypes = 5 # benchmark postStartTime = time.time() postActor = post_json_object['actor'] # ZZZzzz if is_person_snoozed(base_dir, nickname, domain, postActor): return '' # if downloads of avatar images aren't enabled then we can do more # accurate timing of different parts of the code enableTimingLog = not allowDownloads _log_post_timing(enableTimingLog, postStartTime, '1') avatarPosition = '' messageId = '' if post_json_object.get('id'): messageId = remove_hash_from_post_id(post_json_object['id']) messageId = remove_id_ending(messageId) _log_post_timing(enableTimingLog, postStartTime, '2') messageIdStr = '' if messageId: messageIdStr = ';' + messageId domain_full = get_full_domain(domain, port) pageNumberParam = '' if pageNumber: pageNumberParam = '?page=' + str(pageNumber) # get the html post from the recent posts cache if it exists there postHtml = \ _get_post_from_recent_cache(session, base_dir, http_prefix, nickname, domain, post_json_object, postActor, person_cache, allowDownloads, showPublicOnly, storeToCache, boxName, avatarUrl, enableTimingLog, postStartTime, pageNumber, recent_posts_cache, max_recent_posts, signing_priv_key_pem) if postHtml: return postHtml if useCacheOnly and post_json_object['type'] != 'Announce': return '' _log_post_timing(enableTimingLog, postStartTime, '4') avatarUrl = \ get_avatar_image_url(session, base_dir, http_prefix, postActor, person_cache, avatarUrl, allowDownloads, signing_priv_key_pem) _log_post_timing(enableTimingLog, postStartTime, '5') # get the display name if domain_full not in postActor: # lookup the correct webfinger for the postActor postActorNickname = get_nickname_from_actor(postActor) postActorDomain, postActorPort = get_domain_from_actor(postActor) postActorDomainFull = get_full_domain(postActorDomain, postActorPort) postActorHandle = postActorNickname + '@' + postActorDomainFull postActorWf = \ webfinger_handle(session, postActorHandle, http_prefix, cached_webfingers, domain, __version__, False, False, signing_priv_key_pem) avatarUrl2 = None displayName = None if postActorWf: originDomain = domain (inboxUrl, pubKeyId, pubKey, fromPersonId, sharedInbox, avatarUrl2, displayName, _) = get_person_box(signing_priv_key_pem, originDomain, base_dir, session, postActorWf, person_cache, project_version, http_prefix, nickname, domain, 'outbox', 72367) _log_post_timing(enableTimingLog, postStartTime, '6') if avatarUrl2: avatarUrl = avatarUrl2 if displayName: # add any emoji to the display name if ':' in displayName: displayName = \ add_emoji_to_display_name(session, base_dir, http_prefix, nickname, domain, displayName, False) _log_post_timing(enableTimingLog, postStartTime, '7') avatarLink = \ _get_avatar_image_html(showAvatarOptions, nickname, domain_full, avatarUrl, postActor, translate, avatarPosition, pageNumber, messageIdStr) avatarImageInPost = \ '
    ' + avatarLink + '
    \n' timelinePostBookmark = remove_id_ending(post_json_object['id']) timelinePostBookmark = timelinePostBookmark.replace('://', '-') timelinePostBookmark = timelinePostBookmark.replace('/', '-') # If this is the inbox timeline then don't show the repeat icon on any DMs showRepeatIcon = showRepeats isPublicRepeat = False postIsDM = is_dm(post_json_object) if showRepeats: if postIsDM: showRepeatIcon = False else: if not is_public_post(post_json_object): isPublicRepeat = True titleStr = '' galleryStr = '' isAnnounced = False announceJsonObject = None if post_json_object['type'] == 'Announce': announceJsonObject = post_json_object.copy() blockedCache = {} post_jsonAnnounce = \ 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, blockedCache) if not post_jsonAnnounce: # if the announce could not be downloaded then mark it as rejected announcedPostId = remove_id_ending(post_json_object['id']) reject_post_id(base_dir, nickname, domain, announcedPostId, recent_posts_cache) return '' post_json_object = post_jsonAnnounce # is the announced post in the html cache? postHtml = \ _get_post_from_recent_cache(session, base_dir, http_prefix, nickname, domain, post_json_object, postActor, person_cache, allowDownloads, showPublicOnly, storeToCache, boxName, avatarUrl, enableTimingLog, postStartTime, pageNumber, recent_posts_cache, max_recent_posts, signing_priv_key_pem) if postHtml: return postHtml announceFilename = \ locate_post(base_dir, nickname, domain, post_json_object['id']) if announceFilename: update_announce_collection(recent_posts_cache, base_dir, announceFilename, postActor, 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(announceFilename + '.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(announceFilename + '.tts', 'w+') as ttsFile: ttsFile.write('\n') isAnnounced = True _log_post_timing(enableTimingLog, postStartTime, '8') if not has_object_dict(post_json_object): return '' # if this post should be public then check its recipients if showPublicOnly: if not post_contains_public(post_json_object): return '' isModerationPost = False if post_json_object['object'].get('moderationStatus'): isModerationPost = True containerClass = 'container' containerClassIcons = 'containericons' timeClass = 'time-right' actorNickname = get_nickname_from_actor(postActor) if not actorNickname: # single user instance actorNickname = 'dev' actorDomain, actorPort = get_domain_from_actor(postActor) displayName = get_display_name(base_dir, postActor, person_cache) if displayName: if ':' in displayName: displayName = \ add_emoji_to_display_name(session, base_dir, http_prefix, nickname, domain, displayName, False) titleStr += \ ' ' + displayName + '\n' else: if not messageId: # pprint(post_json_object) print('ERROR: no messageId') if not actorNickname: # pprint(post_json_object) print('ERROR: no actorNickname') if not actorDomain: # pprint(post_json_object) print('ERROR: no actorDomain') titleStr += \ ' @' + actorNickname + '@' + actorDomain + '\n' # benchmark 9 _log_post_timing(enableTimingLog, postStartTime, '9') # Show a DM icon for DMs in the inbox timeline if postIsDM: titleStr = \ titleStr + ' \n' # check if replying is permitted commentsEnabled = True if isinstance(post_json_object['object'], dict) and \ 'commentsEnabled' in post_json_object['object']: if post_json_object['object']['commentsEnabled'] is False: commentsEnabled = False elif 'rejectReplies' in post_json_object['object']: if post_json_object['object']['rejectReplies']: commentsEnabled = False conversationId = None if isinstance(post_json_object['object'], dict) and \ 'conversation' in post_json_object['object']: if post_json_object['object']['conversation']: conversationId = post_json_object['object']['conversation'] publicReply = False if is_public_post(post_json_object): publicReply = True replyStr = _get_reply_icon_html(base_dir, nickname, domain, publicReply, showIcons, commentsEnabled, post_json_object, pageNumberParam, translate, system_language, conversationId) _log_post_timing(enableTimingLog, postStartTime, '10') editStr = _get_edit_icon_html(base_dir, nickname, domain_full, post_json_object, actorNickname, translate, False) _log_post_timing(enableTimingLog, postStartTime, '11') announceStr = \ _get_announce_icon_html(isAnnounced, postActor, nickname, domain_full, announceJsonObject, post_json_object, isPublicRepeat, isModerationPost, showRepeatIcon, translate, pageNumberParam, timelinePostBookmark, boxName) _log_post_timing(enableTimingLog, postStartTime, '12') # whether to show a like button hideLikeButtonFile = \ acct_dir(base_dir, nickname, domain) + '/.hideLikeButton' showLikeButton = True if os.path.isfile(hideLikeButtonFile): showLikeButton = False # whether to show a reaction button hideReactionButtonFile = \ acct_dir(base_dir, nickname, domain) + '/.hideReactionButton' showReactionButton = True if os.path.isfile(hideReactionButtonFile): showReactionButton = False likeJsonObject = post_json_object if announceJsonObject: likeJsonObject = announceJsonObject likeStr = _get_like_icon_html(nickname, domain_full, isModerationPost, showLikeButton, likeJsonObject, enableTimingLog, postStartTime, translate, pageNumberParam, timelinePostBookmark, boxName, max_like_count) _log_post_timing(enableTimingLog, postStartTime, '12.5') bookmarkStr = \ _get_bookmark_icon_html(nickname, domain_full, post_json_object, isModerationPost, translate, enableTimingLog, postStartTime, boxName, pageNumberParam, timelinePostBookmark) _log_post_timing(enableTimingLog, postStartTime, '12.9') reactionStr = \ _get_reaction_icon_html(nickname, domain_full, post_json_object, isModerationPost, showReactionButton, translate, enableTimingLog, postStartTime, boxName, pageNumberParam, timelinePostBookmark) _log_post_timing(enableTimingLog, postStartTime, '12.10') is_muted = post_is_muted(base_dir, nickname, domain, post_json_object, messageId) _log_post_timing(enableTimingLog, postStartTime, '13') muteStr = \ _get_mute_icon_html(is_muted, postActor, messageId, nickname, domain_full, allow_deletion, pageNumberParam, boxName, timelinePostBookmark, translate) deleteStr = \ _get_delete_icon_html(nickname, domain_full, allow_deletion, postActor, messageId, post_json_object, pageNumberParam, translate) _log_post_timing(enableTimingLog, postStartTime, '13.1') # get the title: x replies to y, x announces y, etc (titleStr2, replyAvatarImageInPost, containerClassIcons, containerClass) = _get_post_title_html(base_dir, http_prefix, nickname, domain, showRepeatIcon, isAnnounced, post_json_object, postActor, translate, enableTimingLog, postStartTime, boxName, person_cache, allowDownloads, avatarPosition, pageNumber, messageIdStr, containerClassIcons, containerClass) titleStr += titleStr2 _log_post_timing(enableTimingLog, postStartTime, '14') attachmentStr, galleryStr = \ get_post_attachments_as_html(post_json_object, boxName, translate, is_muted, avatarLink, replyStr, announceStr, likeStr, bookmarkStr, deleteStr, muteStr) publishedStr = \ _get_published_date_str(post_json_object, show_published_date_only) _log_post_timing(enableTimingLog, postStartTime, '15') publishedLink = messageId # blog posts should have no /statuses/ in their link if is_blog_post(post_json_object): # is this a post to the local domain? if '://' + domain in messageId: publishedLink = messageId.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 publishedLink or \ domain + ':' + str(port) + '/users/' in publishedLink: publishedLink = '/users/' + publishedLink.split('/users/')[1] if not is_news_post(post_json_object): footerStr = '' + publishedStr + '\n' else: footerStr = '' + publishedStr + '\n' # change the background color for DMs in inbox timeline if postIsDM: containerClassIcons = 'containericons dm' containerClass = 'container dm' newFooterStr = _get_footer_with_icons(showIcons, containerClassIcons, replyStr, announceStr, likeStr, reactionStr, bookmarkStr, deleteStr, muteStr, editStr, post_json_object, publishedLink, timeClass, publishedStr) if newFooterStr: footerStr = newFooterStr # add any content warning from the cwlists directory add_cw_from_lists(post_json_object, cw_lists, translate, lists_enabled) postIsSensitive = False if post_json_object['object'].get('sensitive'): # sensitive posts should have a summary if post_json_object['object'].get('summary'): postIsSensitive = post_json_object['object']['sensitive'] else: # add a generic summary if none is provided sensitiveStr = 'Sensitive' if translate.get(sensitiveStr): sensitiveStr = translate[sensitiveStr] post_json_object['object']['summary'] = sensitiveStr # add an extra line if there is a content warning, # for better vertical spacing on mobile if postIsSensitive: footerStr = '
    ' + footerStr if not post_json_object['object'].get('summary'): post_json_object['object']['summary'] = '' if post_json_object['object'].get('cipherText'): 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) personUrl = local_actor_url(http_prefix, nickname, domain_full) actor_json = \ get_person_from_cache(base_dir, personUrl, person_cache, False) languages_understood = [] if actor_json: languages_understood = get_actor_languages_list(actor_json) contentStr = get_content_from_post(post_json_object, system_language, languages_understood) if not contentStr: contentStr = \ auto_translate_post(base_dir, post_json_object, system_language, translate) if not contentStr: return '' isPatch = is_git_patch(base_dir, nickname, domain, post_json_object['object']['type'], post_json_object['object']['summary'], contentStr) _log_post_timing(enableTimingLog, postStartTime, '16') if not is_pgp_encrypted(contentStr): if not isPatch: objectContent = \ remove_long_words(contentStr, 40, []) objectContent = remove_text_formatting(objectContent) objectContent = limit_repeated_words(objectContent, 6) objectContent = \ switch_words(base_dir, nickname, domain, objectContent) objectContent = html_replace_email_quote(objectContent) objectContent = html_replace_quote_marks(objectContent) else: objectContent = contentStr else: encryptedStr = 'Encrypted' if translate.get(encryptedStr): encryptedStr = translate[encryptedStr] objectContent = '🔒 ' + encryptedStr objectContent = '
    ' + objectContent + '
    ' if not postIsSensitive: contentStr = objectContent + attachmentStr contentStr = add_embedded_elements(translate, contentStr, peertube_instances) contentStr = insert_question(base_dir, translate, nickname, domain, port, contentStr, post_json_object, pageNumber) else: postID = 'post' + str(create_password(8)) contentStr = '' if post_json_object['object'].get('summary'): cwStr = str(post_json_object['object']['summary']) cwStr = \ add_emoji_to_display_name(session, base_dir, http_prefix, nickname, domain, cwStr, False) contentStr += \ '\n ' if isModerationPost: containerClass = 'container report' # get the content warning text cwContentStr = objectContent + attachmentStr if not isPatch: cwContentStr = add_embedded_elements(translate, cwContentStr, peertube_instances) cwContentStr = \ insert_question(base_dir, translate, nickname, domain, port, cwContentStr, post_json_object, pageNumber) cwContentStr = \ switch_words(base_dir, nickname, domain, cwContentStr) if not is_blog_post(post_json_object): # get the content warning button contentStr += \ get_content_warning_button(postID, translate, cwContentStr) else: contentStr += cwContentStr _log_post_timing(enableTimingLog, postStartTime, '17') if post_json_object['object'].get('tag') and not isPatch: contentStr = \ replace_emoji_from_tags(session, base_dir, contentStr, post_json_object['object']['tag'], 'content', False) if is_muted: contentStr = '' else: if not isPatch: contentStr = '
    ' + \ contentStr + \ '
    \n' else: contentStr = \ '
    ' + contentStr + \
                    '
    \n' # show blog citations citationsStr = \ _get_blog_citations_html(boxName, post_json_object, translate) postHtml = '' if boxName != 'tlmedia': reactionStr = '' if showIcons: reactionStr = \ html_emoji_reactions(post_json_object, True, personUrl, maxReactionTypes, boxName, pageNumber) if postIsSensitive and reactionStr: reactionStr = '
    ' + reactionStr postHtml = '
    \n' postHtml += avatarImageInPost postHtml += '
    \n' + \ ' ' + titleStr + \ replyAvatarImageInPost + '
    \n' postHtml += contentStr + citationsStr + reactionStr + footerStr + '\n' postHtml += '
    \n' else: postHtml = galleryStr _log_post_timing(enableTimingLog, postStartTime, '18') # save the created html to the recent posts cache if not showPublicOnly and storeToCache and \ boxName != 'tlmedia' and boxName != 'tlbookmarks' and \ boxName != 'bookmarks': _save_individual_post_as_html_to_cache(base_dir, nickname, domain, post_json_object, postHtml) update_recent_posts_cache(recent_posts_cache, max_recent_posts, post_json_object, postHtml) _log_post_timing(enableTimingLog, postStartTime, '19') return postHtml 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) -> str: """Show an individual post as html """ originalPostJson = post_json_object postStr = '' byStr = '' byText = '' byTextExtra = '' if likedBy: byStr = likedBy byText = 'Liked by' elif reactBy and reactEmoji: byStr = reactBy byText = 'Reaction by' byTextExtra = ' ' + reactEmoji if byStr: byStrNickname = get_nickname_from_actor(byStr) byStrDomain, byStrPort = get_domain_from_actor(byStr) byStrDomain = get_full_domain(byStrDomain, byStrPort) byStrHandle = byStrNickname + '@' + byStrDomain if translate.get(byText): byText = translate[byText] postStr += \ '

    ' + byText + ' @' + \ byStrHandle + '' + byTextExtra + '\n' domain_full = get_full_domain(domain, port) actor = '/users/' + nickname followStr = '

    \n' followStr += \ ' \n' followStr += \ ' \n' if not is_following_actor(base_dir, nickname, domain_full, byStr): translateFollowStr = 'Follow' if translate.get(translateFollowStr): translateFollowStr = translate[translateFollowStr] followStr += ' \n' goBackStr = 'Go Back' if translate.get(goBackStr): goBackStr = translate[goBackStr] followStr += ' \n' followStr += '
    \n' postStr += followStr + '

    \n' postStr += \ 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) messageId = 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: postStr = \ 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) + postStr # show the following posts post_filename = locate_post(base_dir, nickname, domain, messageId) if post_filename: # is there a replies file for this post? repliesFilename = post_filename.replace('.json', '.replies') if os.path.isfile(repliesFilename): # get items from the replies file repliesJson = { 'orderedItems': [] } populate_replies_json(base_dir, nickname, domain, repliesFilename, authorized, repliesJson) # add items to the html output for item in repliesJson['orderedItems']: postStr += \ 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) cssFilename = base_dir + '/epicyon-profile.css' if os.path.isfile(base_dir + '/epicyon.css'): cssFilename = base_dir + '/epicyon.css' instanceTitle = \ get_config_param(base_dir, 'instanceTitle') metadataStr = _html_post_metadata_open_graph(domain, originalPostJson) headerStr = html_header_with_external_style(cssFilename, instanceTitle, metadataStr) return headerStr + postStr + 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, repliesJson: {}, 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) -> str: """Show the replies to an individual post as html """ repliesStr = '' if repliesJson.get('orderedItems'): for item in repliesJson['orderedItems']: repliesStr += \ 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) cssFilename = base_dir + '/epicyon-profile.css' if os.path.isfile(base_dir + '/epicyon.css'): cssFilename = base_dir + '/epicyon.css' instanceTitle = get_config_param(base_dir, 'instanceTitle') metadata = '' headerStr = \ html_header_with_external_style(cssFilename, instanceTitle, metadata) return headerStr + repliesStr + 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, boxName: str, pageNumber: int) -> str: """Returns the emoji picker screen """ reactedToPostStr = \ '
    \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) reactionsFilename = base_dir + '/emoji/reactions.json' if not os.path.isfile(reactionsFilename): reactionsFilename = base_dir + '/emoji/default_reactions.json' reactionsJson = load_json(reactionsFilename) emojiPicksStr = '' baseUrl = '/users/' + nickname post_id = remove_id_ending(post_json_object['id']) for category, item in reactionsJson.items(): emojiPicksStr += '
    \n' for emojiContent in item: emojiContentEncoded = urllib.parse.quote_plus(emojiContent) emojiUrl = \ baseUrl + '?react=' + post_id + \ '?actor=' + post_json_object['actor'] + \ '?tl=' + boxName + \ '?page=' + str(pageNumber) + \ '?emojreact=' + emojiContentEncoded emojiLabel = '' emojiPicksStr += \ ' ' + emojiLabel + '\n' emojiPicksStr += '
    \n' cssFilename = base_dir + '/epicyon-profile.css' if os.path.isfile(base_dir + '/epicyon.css'): cssFilename = base_dir + '/epicyon.css' # filename of the banner shown at the top bannerFile, _ = \ get_banner_file(base_dir, nickname, domain, theme_name) instanceTitle = get_config_param(base_dir, 'instanceTitle') metadata = '' headerStr = \ html_header_with_external_style(cssFilename, instanceTitle, metadata) # banner headerStr += \ '
    \n' + \ '\n' headerStr += '\n' + \ '
    \n' return headerStr + reactedToPostStr + emojiPicksStr + html_footer()