__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,
" 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 + ': ' + byText + ' @' + \
byStrHandle + '' + byTextExtra + '\n'
domain_full = get_full_domain(domain, port)
actor = '/users/' + nickname
followStr = ' \n' + citationsStr + '
\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 ' \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 ' \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 ' \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 = \
'
' + 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 = '' + contentStr + \
'
' + reactionStr
postHtml = '