__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_c_wfrom_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)
if mentionedActors:
for actorUrl in mentionedActors:
if '?mention=' + actorUrl not in replyToLink:
replyToLink += '?mention=' + actorUrl
if len(replyToLink) > 500:
break
replyToLink += pageNumberParam
replyStr = ''
replyToThisPostStr = 'Reply to this post'
if translate.get(replyToThisPostStr):
replyToThisPostStr = translate[replyToThisPostStr]
conversationStr = ''
if conversationId:
conversationStr = '?conversationId=' + conversationId
if isPublicRepeat:
replyStr += \
' \n'
else:
if is_dm(post_json_object):
replyStr += \
' ' + \
'\n'
else:
replyStr += \
' ' + \
'\n'
replyStr += \
' ' + \
'
\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 += \
' ' + \
'' + \
'
\n'
else:
editStr += \
' ' + \
'' + \
'
\n'
elif isEvent:
editEventStr = 'Edit event'
if translate.get(editEventStr):
editEventStr = translate[editEventStr]
editStr += \
' ' + \
'' + \
'
\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 += \
' ' + \
'
\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 += \
' ' + \
'
\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 += \
' ' + \
'
\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 += \
' ' + \
'
\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 += \
' ' + \
'
\n'
else:
undoMuteStr = 'Undo mute'
if translate.get(undoMuteStr):
undoMuteStr = translate[undoMuteStr]
muteStr = \
' \n'
muteStr += \
' ' + \
'
\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 += \
' ' + \
'
\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 += \
'
' + translatedCitationsStr + ':
' + \ '' + contentStr + \
'
' + byText + ' @' + \ byStrHandle + '' + byTextExtra + '\n' domain_full = get_full_domain(domain, port) actor = '/users/' + nickname 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 = \ '