__filename__ = "webapp_post.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.3.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@libreserver.org"
__status__ = "Production"
__module_group__ = "Web Interface"
import os
import time
import urllib.parse
from dateutil.parser import parse
from auth import create_password
from git import is_git_patch
from datetime import datetime
from cache import get_person_from_cache
from bookmarks import bookmarked_by_person
from announce import announced_by_person
from announce import no_of_announces
from like import liked_by_person
from like import no_of_likes
from follow import is_following_actor
from posts import post_is_muted
from posts import get_person_box
from posts import download_announce
from posts import populate_replies_json
from utils import license_link_from_name
from utils import dont_speak_hashtags
from utils import remove_eol
from utils import disallow_announce
from utils import disallow_reply
from utils import convert_published_to_local_timezone
from utils import remove_hash_from_post_id
from utils import remove_html
from utils import get_actor_languages_list
from utils import get_base_content_from_post
from utils import get_content_from_post
from utils import get_language_from_post
from utils import get_summary_from_post
from utils import has_object_dict
from utils import update_announce_collection
from utils import is_pgp_encrypted
from utils import is_dm
from utils import is_reminder
from utils import is_chat_message
from utils import reject_post_id
from utils import is_recent_post
from utils import get_config_param
from utils import get_full_domain
from utils import is_editor
from utils import locate_post
from utils import load_json
from utils import get_cached_post_directory
from utils import get_cached_post_filename
from utils import get_protocol_prefixes
from utils import is_news_post
from utils import is_blog_post
from utils import get_display_name
from utils import display_name_is_emoji
from utils import is_public_post
from utils import is_followers_post
from utils import update_recent_posts_cache
from utils import remove_id_ending
from utils import get_nickname_from_actor
from utils import get_domain_from_actor
from utils import acct_dir
from utils import local_actor_url
from utils import is_unlisted_post
from content import detect_dogwhistles
from content import create_edits_html
from content import bold_reading_string
from content import limit_repeated_words
from content import replace_emoji_from_tags
from content import html_replace_quote_marks
from content import html_replace_email_quote
from content import remove_text_formatting
from content import remove_long_words
from content import get_mentions_from_html
from content import switch_words
from person import is_person_snoozed
from person import get_person_avatar_url
from webapp_utils import language_right_to_left
from webapp_utils import get_banner_file
from webapp_utils import get_avatar_image_url
from webapp_utils import update_avatar_image_cache
from webapp_utils import load_individual_post_as_html_from_cache
from webapp_utils import add_emoji_to_display_name
from webapp_utils import post_contains_public
from webapp_utils import get_content_warning_button
from webapp_utils import get_post_attachments_as_html
from webapp_utils import html_header_with_external_style
from webapp_utils import html_footer
from webapp_utils import get_broken_link_substitute
from webapp_media import add_embedded_elements
from webapp_question import insert_question
from devices import e2e_edecrypt_message_from_device
from webfinger import webfinger_handle
from speaker import update_speaker
from languages import auto_translate_post
from blocking import is_blocked
from blocking import add_cw_from_lists
from reaction import html_emoji_reactions
from maps import html_open_street_map
from maps import set_map_preferences_coords
from maps import set_map_preferences_url
from maps import geocoords_from_map_link
from maps import get_location_from_tags
def _html_post_metadata_open_graph(domain: str, post_json_object: {},
system_language: str) -> str:
"""Returns html OpenGraph metadata for a post
"""
metadata = \
" \n"
metadata += \
" \n"
metadata += \
" \n"
metadata += \
" \n"
obj_json = post_json_object
if has_object_dict(post_json_object):
obj_json = post_json_object['object']
if obj_json.get('id'):
metadata += " \n"
if obj_json.get('summary'):
metadata += " \n"
if obj_json.get('attributedTo'):
if isinstance(obj_json['attributedTo'], str):
attrib = obj_json['attributedTo']
actor_nick = get_nickname_from_actor(attrib)
if actor_nick:
actor_domain, _ = get_domain_from_actor(attrib)
actor_handle = actor_nick + '@' + actor_domain
metadata += \
" \n"
metadata += \
" \n"
if obj_json.get('url'):
metadata += \
" \n"
if obj_json.get('published'):
metadata += " \n"
metadata += \
" \n"
if not obj_json.get('attachment') or obj_json.get('sensitive'):
if obj_json.get('content') and not obj_json.get('sensitive'):
obj_content = obj_json['content']
if obj_json.get('contentMap'):
if obj_json['contentMap'].get(system_language):
obj_content = obj_json['contentMap'][system_language]
description = remove_html(obj_content)
metadata += \
" \n"
metadata += \
" \n"
return metadata
# metadata for attachment
for attach_json in obj_json['attachment']:
if not isinstance(attach_json, dict):
continue
if not attach_json.get('mediaType'):
continue
if not attach_json.get('url'):
continue
if not attach_json.get('name'):
continue
description = None
if attach_json['mediaType'].startswith('image/'):
description = 'Attached: 1 image'
elif attach_json['mediaType'].startswith('video/'):
description = 'Attached: 1 video'
elif attach_json['mediaType'].startswith('audio/'):
description = 'Attached: 1 audio'
if description:
if obj_json.get('content') and not obj_json.get('sensitive'):
obj_content = obj_json['content']
if obj_json.get('contentMap'):
if obj_json['contentMap'].get(system_language):
obj_content = obj_json['contentMap'][system_language]
description += '\n\n' + remove_html(obj_content)
metadata += \
" \n"
metadata += \
" \n"
metadata += \
" \n"
metadata += \
" \n"
if attach_json.get('width'):
metadata += \
" \n"
if attach_json.get('height'):
metadata += \
" \n"
metadata += \
" \n"
if attach_json['mediaType'].startswith('image/'):
metadata += \
" \n"
return metadata
def _log_post_timing(enable_timing_log: bool, post_start_time,
debug_id: str) -> None:
"""Create a log of timings for performance tuning
"""
if not enable_timing_log:
return
time_diff = int((time.time() - post_start_time) * 1000)
if time_diff > 100:
print('TIMING INDIV ' + debug_id + ' = ' + str(time_diff))
def prepare_html_post_nickname(nickname: str, post_html: str) -> str:
"""html posts stored in memory are for all accounts on the instance
and they're indexed by id. However, some incoming posts may be
destined for multiple accounts (followers). This creates a problem
where the icon links whose urls begin with href="/users/nickname?
need to be changed for different nicknames to display correctly
within their timelines.
This function changes the nicknames for the icon links.
"""
# replace the nickname
users_str = ' href="/users/'
if users_str not in post_html:
return post_html
user_found = True
post_str = post_html
new_post_str = ''
while user_found:
if users_str not in post_str:
new_post_str += post_str
break
# the next part, after href="/users/nickname?
next_str = post_str.split(users_str, 1)[1]
if '?' in next_str:
next_str = next_str.split('?', 1)[1]
else:
new_post_str += post_str
break
# append the previous text to the result
new_post_str += post_str.split(users_str)[0]
new_post_str += users_str + nickname + '?'
# post is now the next part
post_str = next_str
return new_post_str
def replace_link_variable(link: str, variable_name: str, value: str,
separator: str = '?') -> str:
"""Replaces a variable within the given link
"""
full_var = separator + variable_name + '='
if full_var not in link:
return link
curr_str = link
result = ''
while full_var in curr_str:
prefix = curr_str.split(full_var, 1)[0] + full_var
next_str = curr_str.split(full_var, 1)[1]
if separator in next_str:
next_str = next_str.split(separator, 1)[1]
result += prefix + value + separator
curr_str = next_str
else:
result += prefix + value
curr_str = ''
return result + curr_str
def prepare_post_from_html_cache(nickname: str, post_html: str, box_name: str,
page_number: int, first_post_id: str) -> str:
"""Sets the page number on a cached html post
"""
# if on the bookmarks timeline then remain there
if box_name in ('tlbookmarks', 'bookmarks'):
post_html = post_html.replace('?tl=inbox', '?tl=tlbookmarks')
if '?page=' in post_html:
page_number_str = post_html.split('?page=')[1]
if '?' in page_number_str:
page_number_str = page_number_str.split('?')[0]
post_html = \
post_html.replace('?page=' + page_number_str, '?page=-999')
# add the page number
with_page_number = \
post_html.replace(';-999;', ';' + str(page_number) + ';')
with_page_number = \
with_page_number.replace('?page=-999', '?page=' + str(page_number))
# add first post in the timeline
if first_post_id is None:
first_post_id = ''
first_post_id = first_post_id.replace('#', '/')
if '?firstpost=' in with_page_number:
with_page_number = \
replace_link_variable(with_page_number,
'firstpost', first_post_id, '?')
elif ';firstpost=' in with_page_number:
with_page_number = \
replace_link_variable(with_page_number,
'firstpost', first_post_id, ';')
else:
with_page_number = \
with_page_number.replace('?page=',
'?firstpost=' + first_post_id +
'?page=')
return prepare_html_post_nickname(nickname, with_page_number)
def _save_individual_post_as_html_to_cache(base_dir: str,
nickname: str, domain: str,
post_json_object: {},
post_html: str) -> bool:
"""Saves the given html for a post to a cache file
This is so that it can be quickly reloaded on subsequent
refresh of the timeline
"""
html_post_cache_dir = \
get_cached_post_directory(base_dir, nickname, domain)
cached_post_filename = \
get_cached_post_filename(base_dir, nickname, domain, post_json_object)
# create the cache directory if needed
if not os.path.isdir(html_post_cache_dir):
os.mkdir(html_post_cache_dir)
try:
with open(cached_post_filename, 'w+', encoding='utf-8') as fp_cache:
fp_cache.write(post_html)
return True
except Exception as ex:
print('ERROR: saving post to cache, ' + str(ex))
return False
def _get_post_from_recent_cache(session,
base_dir: str,
http_prefix: str,
nickname: str, domain: str,
post_json_object: {},
post_actor: str,
person_cache: {},
allow_downloads: bool,
show_public_only: bool,
store_to_cache: bool,
box_name: str,
avatar_url: str,
enable_timing_log: bool,
post_start_time,
page_number: int,
recent_posts_cache: {},
max_recent_posts: int,
signing_priv_key_pem: str,
first_post_id: str) -> str:
"""Attempts to get the html post from the recent posts cache in memory
"""
if box_name == 'tlmedia':
return None
if show_public_only:
return None
try_cache = False
bm_timeline = box_name in ('bookmarks', 'tlbookmarks')
if store_to_cache or bm_timeline:
try_cache = True
if not try_cache:
return None
# update avatar if needed
if not avatar_url:
avatar_url = \
get_person_avatar_url(base_dir, post_actor, person_cache)
_log_post_timing(enable_timing_log, post_start_time, '2.1')
update_avatar_image_cache(signing_priv_key_pem,
session, base_dir, http_prefix,
post_actor, avatar_url, person_cache,
allow_downloads)
_log_post_timing(enable_timing_log, post_start_time, '2.2')
post_html = \
load_individual_post_as_html_from_cache(base_dir, nickname, domain,
post_json_object)
if not post_html:
return None
post_html = \
prepare_post_from_html_cache(nickname, post_html,
box_name, page_number, first_post_id)
update_recent_posts_cache(recent_posts_cache, max_recent_posts,
post_json_object, post_html)
_log_post_timing(enable_timing_log, post_start_time, '3')
return post_html
def _get_avatar_image_html(show_avatar_options: bool,
nickname: str, domain_full: str,
avatar_url: str, post_actor: str,
translate: {}, avatar_position: str,
page_number: int, message_id_str: str) -> str:
"""Get html for the avatar image
"""
# don't use svg images
if avatar_url.endswith('.svg'):
avatar_url = '/icons/avatar_default.png'
avatar_link = ''
if '/users/news/' not in avatar_url:
avatar_link = \
' '
show_profile_str = 'Show profile'
if translate.get(show_profile_str):
show_profile_str = translate[show_profile_str]
avatar_link += \
'\n'
if show_avatar_options and \
domain_full + '/users/' + nickname not in post_actor:
show_options_for_this_person_str = 'Show options for this person'
if translate.get(show_options_for_this_person_str):
show_options_for_this_person_str = \
translate[show_options_for_this_person_str]
if '/users/news/' not in avatar_url:
avatar_link = \
' \n'
avatar_link += \
' \n'
else:
# don't link to the person options for the news account
avatar_link += \
' \n'
return avatar_link.strip()
def _get_reply_icon_html(base_dir: str, nickname: str, domain: str,
is_public_reply: bool, is_unlisted_reply: bool,
show_icons: bool, comments_enabled: bool,
post_json_object: {}, page_number_param: str,
translate: {}, system_language: str,
conversation_id: str) -> str:
"""Returns html for the reply icon/button
"""
reply_str = ''
if not (show_icons and comments_enabled):
return reply_str
# reply is permitted - create reply icon
reply_to_link = remove_hash_from_post_id(post_json_object['object']['id'])
reply_to_link = remove_id_ending(reply_to_link)
# see Mike MacGirvin's replyTo suggestion
if post_json_object['object'].get('replyTo'):
# check that the alternative replyTo url is not blocked
block_nickname = \
get_nickname_from_actor(post_json_object['object']['replyTo'])
if not block_nickname:
return reply_str
block_domain, _ = \
get_domain_from_actor(post_json_object['object']['replyTo'])
if not is_blocked(base_dir, nickname, domain,
block_nickname, block_domain, {}):
reply_to_link = post_json_object['object']['replyTo']
if post_json_object['object'].get('attributedTo'):
if isinstance(post_json_object['object']['attributedTo'], str):
reply_to_link += \
'?mention=' + post_json_object['object']['attributedTo']
content = get_base_content_from_post(post_json_object, system_language)
if content:
mentioned_actors = \
get_mentions_from_html(content,
" 500:
break
reply_to_link += page_number_param
reply_str = ''
reply_to_this_post_str = 'Reply to this post'
if translate.get(reply_to_this_post_str):
reply_to_this_post_str = translate[reply_to_this_post_str]
conversation_str = ''
if conversation_id:
conversation_str = '?conversationId=' + conversation_id
if is_public_reply:
reply_str += \
' \n'
elif is_unlisted_reply:
reply_str += \
' \n'
else:
if is_dm(post_json_object):
reply_type = 'replydm'
if is_chat_message(post_json_object):
reply_type = 'replychat'
reply_str += \
' ' + \
'\n'
else:
reply_str += \
' ' + \
'\n'
reply_str += \
' ' + \
'\n'
return reply_str
def _get_edit_icon_html(base_dir: str, nickname: str, domain_full: str,
post_json_object: {}, actor_nickname: str,
translate: {}, is_event: bool,
first_post_id: str) -> str:
"""Returns html for the edit icon/button
"""
edit_str = ''
actor = post_json_object['actor']
# This should either be a post which you created,
# or it could be generated from the newswire (see
# _add_blogs_to_newswire) in which case anyone with
# editor status should be able to alter it
if (actor.endswith('/' + domain_full + '/users/' + nickname) or
(is_editor(base_dir, nickname) and
actor.endswith('/' + domain_full + '/users/news'))):
post_id = remove_id_ending(post_json_object['object']['id'])
if '/statuses/' not in post_id:
return edit_str
reply_to = ''
if post_json_object['object'].get('inReplyTo'):
reply_to = ';replyTo=' + post_json_object['object']['inReplyTo']
first_post_str = ''
if first_post_id:
first_post_str = ';firstpost=' + first_post_id
if is_blog_post(post_json_object):
edit_blog_post_str = 'Edit blog post'
if translate.get(edit_blog_post_str):
edit_blog_post_str = translate[edit_blog_post_str]
if not is_news_post(post_json_object):
edit_str += \
' ' + \
'' + \
'\n'
else:
edit_str += \
' ' + \
'' + \
'\n'
elif is_event:
edit_event_str = 'Edit event'
if translate.get(edit_event_str):
edit_event_str = translate[edit_event_str]
edit_str += \
' ' + \
'' + \
'\n'
elif is_public_post(post_json_object):
# Edit a public post
edit_post_str = 'Edit post'
if translate.get(edit_post_str):
edit_post_str = translate[edit_post_str]
edit_str += \
' ' + \
'' + \
'\n'
elif is_reminder(post_json_object):
# Edit a reminder
edit_post_str = 'Edit reminder'
if translate.get(edit_post_str):
edit_post_str = translate[edit_post_str]
edit_str += \
' ' + \
'' + \
'\n'
elif is_dm(post_json_object):
# Edit a DM
edit_post_str = 'Edit post'
if translate.get(edit_post_str):
edit_post_str = translate[edit_post_str]
edit_str += \
' ' + \
'' + \
'\n'
elif is_unlisted_post(post_json_object):
# Edit an unlisted post
edit_post_str = 'Edit post'
if translate.get(edit_post_str):
edit_post_str = translate[edit_post_str]
edit_str += \
' ' + \
'' + \
'\n'
elif is_followers_post(post_json_object):
# Edit a followers only post
edit_post_str = 'Edit post'
if translate.get(edit_post_str):
edit_post_str = translate[edit_post_str]
edit_str += \
' ' + \
'' + \
'\n'
return edit_str
def _get_announce_icon_html(is_announced: bool,
post_actor: str,
nickname: str, domain_full: str,
announce_json_object: {},
post_json_object: {},
is_public_repeat: bool,
is_moderation_post: bool,
show_repeat_icon: bool,
translate: {},
page_number_param: str,
timeline_post_bookmark: str,
box_name: str,
max_announce_count: int,
first_post_id: str) -> str:
"""Returns html for announce icon/button at the bottom of the post
"""
announce_str = ''
if not show_repeat_icon:
return announce_str
if is_moderation_post:
return announce_str
# don't allow announce/repeat of your own posts
announce_icon = 'repeat_inactive.png'
announce_link = 'repeat'
announce_emoji = ''
if not is_public_repeat:
announce_link = 'repeatprivate'
repeat_this_post_str = 'Repeat this post'
if translate.get(repeat_this_post_str):
repeat_this_post_str = translate[repeat_this_post_str]
announce_title = repeat_this_post_str
unannounce_link_str = ''
announce_count = no_of_announces(post_json_object)
announce_count_str = ''
if announce_count > 0:
if announce_count <= max_announce_count:
announce_count_str = ' (' + str(announce_count) + ')'
else:
announce_count_str = ' (' + str(max_announce_count) + '+)'
if announced_by_person(is_announced,
post_actor, nickname, domain_full):
if announce_count == 1:
# announced by the reader only
announce_count_str = ''
announce_icon = 'repeat.png'
announce_emoji = '🔁 '
announce_link = 'unrepeat'
if not is_public_repeat:
announce_link = 'unrepeatprivate'
undo_the_repeat_str = 'Undo the repeat'
if translate.get(undo_the_repeat_str):
undo_the_repeat_str = translate[undo_the_repeat_str]
announce_title = undo_the_repeat_str
if announce_json_object:
unannounce_link_str = '?unannounce=' + \
remove_id_ending(announce_json_object['id'])
announce_post_id = \
remove_hash_from_post_id(post_json_object['object']['id'])
announce_post_id = remove_id_ending(announce_post_id)
announce_str = ''
if announce_count_str:
announcers_post_id = announce_post_id.replace('/', '--')
announcers_screen_link = \
'/users/' + nickname + '?announcers=' + announcers_post_id
# show the number of announces next to icon
announce_str += '\n'
first_post_str = ''
if first_post_id:
first_post_str = '?firstpost=' + first_post_id
announce_link_str = '?' + \
announce_link + '=' + announce_post_id + page_number_param
announce_str += \
' \n'
announce_str += \
' ' + \
'\n'
return announce_str
def _get_like_icon_html(nickname: str, domain_full: str,
is_moderation_post: bool,
show_like_button: bool,
post_json_object: {},
enable_timing_log: bool,
post_start_time,
translate: {}, page_number_param: str,
timeline_post_bookmark: str,
box_name: str,
max_like_count: int,
first_post_id: str) -> str:
"""Returns html for like icon/button
"""
if not show_like_button or is_moderation_post:
return ''
like_str = ''
like_icon = 'like_inactive.png'
like_link = 'like'
like_title = 'Like this post'
if translate.get(like_title):
like_title = translate[like_title]
like_emoji = ''
like_count = no_of_likes(post_json_object)
_log_post_timing(enable_timing_log, post_start_time, '12.1')
like_count_str = ''
if like_count > 0:
if like_count <= max_like_count:
like_count_str = ' (' + str(like_count) + ')'
else:
like_count_str = ' (' + str(max_like_count) + '+)'
if liked_by_person(post_json_object, nickname, domain_full):
if like_count == 1:
# liked by the reader only
like_count_str = ''
like_icon = 'like.png'
like_link = 'unlike'
like_title = 'Undo the like'
if translate.get(like_title):
like_title = translate[like_title]
like_emoji = '👍 '
_log_post_timing(enable_timing_log, post_start_time, '12.2')
like_post_id = remove_hash_from_post_id(post_json_object['id'])
like_post_id = remove_id_ending(like_post_id)
like_str = ''
if like_count_str:
likers_post_id = like_post_id.replace('/', '--')
likers_screen_link = \
'/users/' + nickname + '?likers=' + likers_post_id
# show the number of likes next to icon
like_str += '\n'
first_post_str = ''
if first_post_id:
first_post_str = '?firstpost=' + first_post_id
like_str += \
' \n'
like_str += \
' ' + \
'\n'
return like_str
def _get_bookmark_icon_html(nickname: str, domain_full: str,
post_json_object: {},
is_moderation_post: bool,
translate: {},
enable_timing_log: bool,
post_start_time, box_name: str,
page_number_param: str,
timeline_post_bookmark: str,
first_post_id: str) -> str:
"""Returns html for bookmark icon/button
"""
bookmark_str = ''
if is_moderation_post:
return bookmark_str
bookmark_icon = 'bookmark_inactive.png'
bookmark_link = 'bookmark'
bookmark_emoji = ''
bookmark_title = 'Bookmark this post'
if translate.get(bookmark_title):
bookmark_title = translate[bookmark_title]
if bookmarked_by_person(post_json_object, nickname, domain_full):
bookmark_icon = 'bookmark.png'
bookmark_link = 'unbookmark'
bookmark_emoji = '🔖 '
bookmark_title = 'Undo the bookmark'
if translate.get(bookmark_title):
bookmark_title = translate[bookmark_title]
_log_post_timing(enable_timing_log, post_start_time, '12.6')
bookmark_post_id = \
remove_hash_from_post_id(post_json_object['object']['id'])
bookmark_post_id = remove_id_ending(bookmark_post_id)
first_post_str = ''
if first_post_id:
first_post_str = '?firstpost=' + first_post_id
bookmark_str = \
' \n'
bookmark_str += \
' ' + \
'\n'
return bookmark_str
def _get_reaction_icon_html(nickname: str, post_json_object: {},
is_moderation_post: bool,
show_reaction_button: bool,
translate: {},
enable_timing_log: bool,
post_start_time, box_name: str,
page_number_param: str,
timeline_post_reaction: str,
first_post_id: str) -> str:
"""Returns html for reaction icon/button
"""
reaction_str = ''
if not show_reaction_button or is_moderation_post:
return reaction_str
reaction_icon = 'reaction.png'
reaction_title = 'Select reaction'
if translate.get(reaction_title):
reaction_title = translate[reaction_title]
_log_post_timing(enable_timing_log, post_start_time, '12.65')
reaction_post_id = \
remove_hash_from_post_id(post_json_object['object']['id'])
reaction_post_id = remove_id_ending(reaction_post_id)
first_post_str = ''
if first_post_id:
first_post_str = '?firstpost=' + first_post_id
reaction_str = \
' \n'
reaction_str += \
' ' + \
'\n'
return reaction_str
def _get_mute_icon_html(is_muted: bool,
post_actor: str,
message_id: str,
nickname: str, domain_full: str,
allow_deletion: bool,
page_number_param: str,
box_name: str,
timeline_post_bookmark: str,
translate: {},
first_post_id: str) -> str:
"""Returns html for mute icon/button
"""
mute_str = ''
if (allow_deletion or
('/' + domain_full + '/' in post_actor and
message_id.startswith(post_actor))):
return mute_str
first_post_str = ''
if first_post_id:
first_post_str = '?firstpost=' + first_post_id
if not is_muted:
mute_this_post_str = 'Mute this post'
if translate.get('Mute this post'):
mute_this_post_str = translate[mute_this_post_str]
mute_str = \
' \n'
mute_str += \
' ' + \
'\n'
else:
undo_mute_str = 'Undo mute'
if translate.get(undo_mute_str):
undo_mute_str = translate[undo_mute_str]
mute_str = \
' \n'
mute_str += \
' ' + \
'\n'
return mute_str
def _get_delete_icon_html(nickname: str, domain_full: str,
allow_deletion: bool,
post_actor: str,
message_id: str,
post_json_object: {},
page_number_param: str,
translate: {},
first_post_id: str) -> str:
"""Returns html for delete icon/button
"""
delete_str = ''
if (allow_deletion or
('/' + domain_full + '/' in post_actor and
message_id.startswith(post_actor))):
if '/users/' + nickname + '/' in message_id:
if not is_news_post(post_json_object):
delete_this_post_str = 'Delete this post'
if translate.get(delete_this_post_str):
delete_this_post_str = translate[delete_this_post_str]
first_post_str = ''
if first_post_id:
first_post_str = '?firstpost=' + first_post_id
delete_str = \
' \n'
delete_str += \
' ' + \
'\n'
return delete_str
def _get_published_date_str(post_json_object: {},
show_published_date_only: bool,
timezone: str) -> str:
"""Return the html for the published date on a post
"""
published_str = ''
if not post_json_object['object'].get('published'):
return published_str
published_str = post_json_object['object']['published']
if '.' not in published_str:
if '+' not in published_str:
datetime_object = \
datetime.strptime(published_str, "%Y-%m-%dT%H:%M:%SZ")
else:
datetime_object = \
datetime.strptime(published_str.split('+')[0] + 'Z',
"%Y-%m-%dT%H:%M:%SZ")
else:
published_str = \
published_str.replace('T', ' ').split('.')[0]
datetime_object = parse(published_str)
# convert to local time
datetime_object = \
convert_published_to_local_timezone(datetime_object, timezone)
if not show_published_date_only:
published_str = datetime_object.strftime("%a %b %d, %H:%M")
else:
published_str = datetime_object.strftime("%a %b %d")
# if the post has replies then append a symbol to indicate this
if post_json_object.get('hasReplies'):
if post_json_object['hasReplies'] is True:
published_str = '[' + published_str + ']'
return published_str
def _get_blog_citations_html(box_name: str,
post_json_object: {},
translate: {}) -> str:
"""Returns blog citations as html
"""
# show blog citations
citations_str = ''
if box_name not in ('tlblogs', 'tlfeatures'):
return citations_str
if not post_json_object['object'].get('tag'):
return citations_str
for tag_json in post_json_object['object']['tag']:
if not isinstance(tag_json, dict):
continue
if not tag_json.get('type'):
continue
if tag_json['type'] != 'Article':
continue
if not tag_json.get('name'):
continue
if not tag_json.get('url'):
continue
citations_str += \
' ' + translated_citations_str + ': ' + by_text + ' @' + \
by_str_handle + '' + by_text_extra + '\n'
domain_full = get_full_domain(domain, port)
actor = '/users/' + nickname
follow_str = '
' + footer_str
if not summary_str:
summary_str = get_summary_from_post(post_json_object, system_language,
languages_understood)
is_patch = is_git_patch(base_dir, nickname, domain,
post_json_object['object']['type'],
summary_str, content_str)
_log_post_timing(enable_timing_log, post_start_time, '16')
if not is_pgp_encrypted(content_str):
# if we are on an onion instance then substitute any common clearnet
# domains with their onion version
if '.onion' in domain and '://' in content_str:
content_str = \
_substitute_onion_domains(base_dir, content_str)
if not is_patch:
# remove any tabs
content_str = \
content_str.replace('\t', '').replace('\r', '')
# Add bold text
if bold_reading and \
not displaying_ciphertext and \
not post_is_blog:
content_str = bold_reading_string(content_str)
object_content = \
remove_long_words(content_str, 40, [])
object_content = \
remove_text_formatting(object_content, bold_reading)
object_content = limit_repeated_words(object_content, 6)
object_content = \
switch_words(base_dir, nickname, domain, object_content)
object_content = html_replace_email_quote(object_content)
object_content = html_replace_quote_marks(object_content)
# append any edits
object_content += edits_str
else:
object_content = content_str
else:
encrypted_str = 'Encrypted'
if translate.get(encrypted_str):
encrypted_str = translate[encrypted_str]
object_content = '🔒 ' + encrypted_str
object_content = \
'' + content_str + \
'
' + reaction_str
post_html = '