epicyon/webapp_utils.py

2376 lines
96 KiB
Python
Raw Normal View History

2020-11-09 15:22:59 +00:00
__filename__ = "webapp_utils.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
2024-01-21 19:01:20 +00:00
__version__ = "1.5.0"
2020-11-09 15:22:59 +00:00
__maintainer__ = "Bob Mottram"
2021-09-10 16:14:50 +00:00
__email__ = "bob@libreserver.org"
2020-11-09 15:22:59 +00:00
__status__ = "Production"
2021-06-15 15:08:12 +00:00
__module_group__ = "Web Interface"
2020-11-09 15:22:59 +00:00
import os
2021-10-30 11:08:57 +00:00
from shutil import copyfile
2020-11-09 15:22:59 +00:00
from collections import OrderedDict
2021-12-29 21:55:09 +00:00
from session import get_json
2023-08-13 09:58:02 +00:00
from session import get_json_valid
2024-02-05 20:05:00 +00:00
from utils import image_mime_types_dict
2023-12-09 14:18:24 +00:00
from utils import get_url_from_post
from utils import get_media_url_from_video
from utils import get_attributed_to
from utils import local_network_host
from utils import dangerous_markup
2022-12-18 15:29:54 +00:00
from utils import acct_handle_dir
from utils import remove_id_ending
from utils import get_attachment_property_value
2021-12-26 18:46:43 +00:00
from utils import is_account_dir
2021-12-27 15:43:22 +00:00
from utils import remove_html
2021-12-27 17:20:01 +00:00
from utils import get_protocol_prefixes
2021-12-26 15:13:34 +00:00
from utils import load_json
2021-12-26 23:41:34 +00:00
from utils import get_cached_post_filename
2021-12-26 14:08:58 +00:00
from utils import get_config_param
2021-12-26 12:02:29 +00:00
from utils import acct_dir
2021-12-27 22:19:18 +00:00
from utils import get_nickname_from_actor
from utils import get_domain_from_actor
2021-12-26 18:03:39 +00:00
from utils import is_float
2021-12-26 14:24:03 +00:00
from utils import get_audio_extensions
2021-12-26 14:20:09 +00:00
from utils import get_video_extensions
2021-12-26 14:26:16 +00:00
from utils import get_image_extensions
2021-12-26 10:19:59 +00:00
from utils import local_actor_url
2022-06-10 13:01:39 +00:00
from utils import text_in_file
2022-06-21 11:58:50 +00:00
from utils import remove_eol
from utils import binary_is_image
2024-01-27 17:04:21 +00:00
from utils import resembles_url
from filters import is_filtered
from cache import get_actor_public_key_from_id
2021-12-29 21:55:09 +00:00
from cache import store_person_in_cache
from content import add_html_tags
from content import replace_emoji_from_tags
from person import get_person_avatar_url
2023-12-20 18:22:28 +00:00
from person import get_person_notes
2021-12-28 19:33:29 +00:00
from posts import is_moderator
2021-12-29 21:55:09 +00:00
from blocking import is_blocked
from blocking import allowed_announce
from shares import vf_proposal_from_share
2023-09-29 14:47:44 +00:00
from webapp_pwa import get_pwa_theme_colors
2020-11-09 15:22:59 +00:00
def minimizing_attached_images(base_dir: str, nickname: str, domain: str,
following_nickname: str,
following_domain: str) -> bool:
"""Returns true if images from the account being followed should be
minimized by default
"""
if following_nickname == nickname and following_domain == domain:
# reminder post
return False
minimize_filename = \
acct_dir(base_dir, nickname, domain) + '/followingMinimizeImages.txt'
handle = following_nickname + '@' + following_domain
if not os.path.isfile(minimize_filename):
following_filename = \
acct_dir(base_dir, nickname, domain) + '/following.txt'
if not os.path.isfile(following_filename):
return False
# create a new minimize file from the following file
try:
2022-06-11 16:19:37 +00:00
with open(minimize_filename, 'w+',
encoding='utf-8') as fp_min:
fp_min.write('')
except OSError:
2022-06-11 16:19:37 +00:00
print('EX: minimizing_attached_images 2 ' + minimize_filename)
2022-12-30 20:50:43 +00:00
return text_in_file(handle + '\n', minimize_filename, False)
2021-12-29 21:55:09 +00:00
def get_broken_link_substitute() -> str:
"""Returns html used to show a default image if the link to
an image is broken
"""
return " onerror=\"this.onerror=null; this.src='" + \
"/icons/avatar_default.png'\""
2022-06-01 17:45:59 +00:00
def html_following_list(base_dir: str, following_filename: str) -> str:
2020-11-28 10:49:10 +00:00
"""Returns a list of handles being followed
"""
2022-06-09 14:46:30 +00:00
with open(following_filename, 'r', encoding='utf-8') as following_file:
2022-01-04 14:33:19 +00:00
msg = following_file.read()
following_list = msg.split('\n')
following_list.sort()
if following_list:
2021-12-31 21:18:12 +00:00
css_filename = base_dir + '/epicyon-profile.css'
2021-12-25 16:17:53 +00:00
if os.path.isfile(base_dir + '/epicyon.css'):
2021-12-31 21:18:12 +00:00
css_filename = base_dir + '/epicyon.css'
2020-11-28 10:49:10 +00:00
2022-01-04 14:33:19 +00:00
instance_title = \
2021-12-26 14:08:58 +00:00
get_config_param(base_dir, 'instanceTitle')
2022-01-04 14:33:19 +00:00
following_list_html = \
2021-12-31 21:18:12 +00:00
html_header_with_external_style(css_filename,
2022-01-04 14:33:19 +00:00
instance_title, None)
for following_address in following_list:
if following_address:
following_list_html += \
'<h3>@' + following_address + '</h3>'
following_list_html += html_footer()
msg = following_list_html
2020-11-28 10:49:10 +00:00
return msg
return ''
2022-11-07 18:00:06 +00:00
def csv_following_list(following_filename: str,
base_dir: str, nickname: str, domain: str) -> str:
2022-07-20 13:04:09 +00:00
"""Returns a csv of handles being followed
"""
with open(following_filename, 'r', encoding='utf-8') as following_file:
msg = following_file.read()
following_list = msg.split('\n')
following_list.sort()
if following_list:
following_list_csv = ''
for following_address in following_list:
if not following_address:
continue
following_nickname = \
get_nickname_from_actor(following_address)
following_domain, _ = \
get_domain_from_actor(following_address)
announce_is_allowed = \
allowed_announce(base_dir, nickname, domain,
following_nickname,
following_domain)
2022-11-22 09:43:29 +00:00
notify_on_new = 'false'
languages = ''
2023-12-20 18:22:28 +00:00
person_notes = \
get_person_notes(base_dir, nickname, domain,
following_address)
if person_notes:
# make notes suitable for csv file
person_notes = person_notes.replace(',', ' ')
person_notes = person_notes.replace('"', "'")
person_notes = person_notes.replace('\n', '<br>')
person_notes = person_notes.replace(' ', ' ')
2022-12-09 18:50:24 +00:00
if not following_list_csv:
following_list_csv = \
'Account address,Show boosts,' + \
'Notify on new posts,Languages,Notes\n'
2022-11-22 09:43:29 +00:00
following_list_csv += \
following_address + ',' + \
str(announce_is_allowed).lower() + ',' + \
notify_on_new + ',' + \
languages + ',' + \
person_notes + '\n'
msg = following_list_csv
2022-07-20 13:04:09 +00:00
return msg
return ''
2022-06-01 17:45:59 +00:00
def html_hashtag_blocked(base_dir: str, translate: {}) -> str:
2020-11-28 10:49:10 +00:00
"""Show the screen for a blocked hashtag
"""
2022-01-04 14:33:19 +00:00
blocked_hashtag_form = ''
2021-12-31 21:18:12 +00:00
css_filename = base_dir + '/epicyon-suspended.css'
2021-12-25 16:17:53 +00:00
if os.path.isfile(base_dir + '/suspended.css'):
2021-12-31 21:18:12 +00:00
css_filename = base_dir + '/suspended.css'
2020-11-28 10:49:10 +00:00
2022-01-04 14:33:19 +00:00
instance_title = \
2021-12-26 14:08:58 +00:00
get_config_param(base_dir, 'instanceTitle')
2022-01-04 14:33:19 +00:00
blocked_hashtag_form = \
html_header_with_external_style(css_filename, instance_title, None)
blocked_hashtag_form += '<div><center>\n'
blocked_hashtag_form += \
2020-11-28 10:49:10 +00:00
' <p class="screentitle">' + \
translate['Hashtag Blocked'] + '</p>\n'
2022-01-04 14:33:19 +00:00
blocked_hashtag_form += \
2020-11-28 10:49:10 +00:00
' <p>See <a href="/terms">' + \
translate['Terms of Service'] + '</a></p>\n'
2022-01-04 14:33:19 +00:00
blocked_hashtag_form += '</center></div>\n'
blocked_hashtag_form += html_footer()
return blocked_hashtag_form
2020-11-28 10:49:10 +00:00
2021-12-29 21:55:09 +00:00
def header_buttons_front_screen(translate: {},
2022-01-04 14:33:19 +00:00
nickname: str, box_name: str,
2021-12-29 21:55:09 +00:00
authorized: bool,
icons_as_buttons: bool) -> str:
2020-11-28 10:19:59 +00:00
"""Returns the header buttons for the front page of a news instance
"""
2022-01-04 14:33:19 +00:00
header_str = ''
2020-11-28 10:19:59 +00:00
if nickname == 'news':
2022-01-04 14:33:19 +00:00
button_features = 'buttonMobile'
button_newswire = 'buttonMobile'
button_links = 'buttonMobile'
if box_name == 'features':
button_features = 'buttonselected'
elif box_name == 'newswire':
button_newswire = 'buttonselected'
elif box_name == 'links':
button_links = 'buttonselected'
header_str += \
2020-11-28 10:19:59 +00:00
' <a href="/">' + \
2022-01-04 14:33:19 +00:00
'<button class="' + button_features + '">' + \
2020-11-28 10:19:59 +00:00
'<span>' + translate['Features'] + \
'</span></button></a>'
if not authorized:
2022-01-04 14:33:19 +00:00
header_str += \
2020-11-28 10:19:59 +00:00
' <a href="/login">' + \
'<button class="buttonMobile">' + \
'<span>' + translate['Login'] + \
'</span></button></a>'
2021-12-25 19:19:14 +00:00
if icons_as_buttons:
2022-01-04 14:33:19 +00:00
header_str += \
2020-11-28 10:19:59 +00:00
' <a href="/users/news/newswiremobile">' + \
2022-01-04 14:33:19 +00:00
'<button class="' + button_newswire + '">' + \
2020-11-28 10:19:59 +00:00
'<span>' + translate['Newswire'] + \
'</span></button></a>'
2022-01-04 14:33:19 +00:00
header_str += \
2020-11-28 10:19:59 +00:00
' <a href="/users/news/linksmobile">' + \
2022-01-04 14:33:19 +00:00
'<button class="' + button_links + '">' + \
2020-11-28 10:19:59 +00:00
'<span>' + translate['Links'] + \
'</span></button></a>'
else:
2022-01-04 14:33:19 +00:00
header_str += \
2020-11-28 10:19:59 +00:00
' <a href="' + \
'/users/news/newswiremobile">' + \
2022-03-28 08:47:53 +00:00
'<img loading="lazy" decoding="async" src="/icons' + \
2020-11-28 10:19:59 +00:00
'/newswire.png" title="' + translate['Newswire'] + \
'" alt="| ' + translate['Newswire'] + '"/></a>\n'
2022-01-04 14:33:19 +00:00
header_str += \
2020-11-28 10:19:59 +00:00
' <a href="' + \
'/users/news/linksmobile">' + \
2022-03-28 08:47:53 +00:00
'<img loading="lazy" decoding="async" src="/icons' + \
2020-11-28 10:19:59 +00:00
'/links.png" title="' + translate['Links'] + \
'" alt="| ' + translate['Links'] + '"/></a>\n'
else:
if not authorized:
2022-01-04 14:33:19 +00:00
header_str += \
2020-11-28 10:19:59 +00:00
' <a href="/login">' + \
'<button class="buttonMobile">' + \
'<span>' + translate['Login'] + \
'</span></button></a>'
2022-01-04 14:33:19 +00:00
if header_str:
header_str = \
2020-11-28 10:19:59 +00:00
'\n <div class="frontPageMobileButtons">\n' + \
2022-01-04 14:33:19 +00:00
header_str + \
2020-11-28 10:19:59 +00:00
' </div>\n'
2022-01-04 14:33:19 +00:00
return header_str
2020-11-28 10:19:59 +00:00
2022-01-04 14:33:19 +00:00
def get_content_warning_button(post_id: str, translate: {},
2021-12-29 21:55:09 +00:00
content: str) -> str:
2020-11-09 15:22:59 +00:00
"""Returns the markup for a content warning button
"""
2022-06-09 17:38:47 +00:00
return ' <details><summary class="cw" tabindex="10">' + \
2021-01-19 19:24:16 +00:00
translate['SHOW MORE'] + '</summary>' + \
2022-01-04 14:33:19 +00:00
'<div id="' + post_id + '">' + content + \
2020-11-09 15:22:59 +00:00
'</div></details>\n'
2021-12-29 21:55:09 +00:00
def _set_actor_property_url(actor_json: {},
property_name: str, url: str) -> None:
2020-11-09 15:22:59 +00:00
"""Sets a url for the given actor property
"""
2021-12-26 10:29:52 +00:00
if not actor_json.get('attachment'):
actor_json['attachment'] = []
2020-11-09 15:22:59 +00:00
2022-01-04 14:33:19 +00:00
property_name_lower = property_name.lower()
2020-11-09 15:22:59 +00:00
# remove any existing value
2022-01-03 10:27:55 +00:00
property_found = None
2021-12-26 10:32:45 +00:00
for property_value in actor_json['attachment']:
2022-05-11 16:10:38 +00:00
name_value = None
if property_value.get('name'):
name_value = property_value['name']
elif property_value.get('schema:name'):
name_value = property_value['schema:name']
if not name_value:
2020-11-09 15:22:59 +00:00
continue
2021-12-26 10:32:45 +00:00
if not property_value.get('type'):
2020-11-09 15:22:59 +00:00
continue
2022-05-11 16:10:38 +00:00
if not name_value.lower().startswith(property_name_lower):
2020-11-09 15:22:59 +00:00
continue
2022-01-03 10:27:55 +00:00
property_found = property_value
2020-11-09 15:22:59 +00:00
break
2022-01-03 10:27:55 +00:00
if property_found:
actor_json['attachment'].remove(property_found)
2020-11-09 15:22:59 +00:00
2021-12-27 17:20:01 +00:00
prefixes = get_protocol_prefixes()
2022-01-04 14:33:19 +00:00
prefix_found = False
2020-11-09 15:22:59 +00:00
for prefix in prefixes:
if url.startswith(prefix):
2022-01-04 14:33:19 +00:00
prefix_found = True
2020-11-09 15:22:59 +00:00
break
2022-01-04 14:33:19 +00:00
if not prefix_found:
2020-11-09 15:22:59 +00:00
return
if '.' not in url:
return
if ' ' in url:
return
if ',' in url:
return
2021-12-26 10:32:45 +00:00
for property_value in actor_json['attachment']:
2022-05-11 16:10:38 +00:00
name_value = None
if property_value.get('name'):
name_value = property_value['name']
elif property_value.get('schema:name'):
name_value = property_value['schema:name']
if not name_value:
2020-11-09 15:22:59 +00:00
continue
2021-12-26 10:32:45 +00:00
if not property_value.get('type'):
2020-11-09 15:22:59 +00:00
continue
2022-05-11 16:10:38 +00:00
if not name_value.lower().startswith(property_name_lower):
2020-11-09 15:22:59 +00:00
continue
2022-05-11 16:16:34 +00:00
if not property_value['type'].endswith('PropertyValue'):
2020-11-09 15:22:59 +00:00
continue
prop_value_name, _ = \
get_attachment_property_value(property_value)
if not prop_value_name:
continue
property_value[prop_value_name] = url
2020-11-09 15:22:59 +00:00
return
2022-01-04 14:33:19 +00:00
new_address = {
2021-12-26 18:19:58 +00:00
"name": property_name,
2020-11-09 15:22:59 +00:00
"type": "PropertyValue",
"value": url
}
2022-01-04 14:33:19 +00:00
actor_json['attachment'].append(new_address)
2020-11-09 15:22:59 +00:00
def set_blog_address(actor_json: {}, blog_address: str) -> None:
2020-11-09 15:22:59 +00:00
"""Sets an blog address for the given actor
"""
_set_actor_property_url(actor_json, 'Blog', remove_html(blog_address))
2020-11-09 15:22:59 +00:00
2021-12-29 21:55:09 +00:00
def update_avatar_image_cache(signing_priv_key_pem: str,
session, base_dir: str, http_prefix: str,
2022-01-04 14:33:19 +00:00
actor: str, avatar_url: str,
person_cache: {}, allow_downloads: bool,
2021-12-29 21:55:09 +00:00
force: bool = False, debug: bool = False) -> str:
2020-11-09 15:22:59 +00:00
"""Updates the cached avatar for the given actor
"""
2022-01-04 14:33:19 +00:00
if not avatar_url:
2020-11-09 15:22:59 +00:00
return None
2022-01-04 14:33:19 +00:00
actor_str = actor.replace('/', '-')
avatar_image_path = base_dir + '/cache/avatars/' + actor_str
2020-12-12 14:23:14 +00:00
# try different image types
2024-02-05 20:05:00 +00:00
image_formats = image_mime_types_dict()
2022-01-04 14:33:19 +00:00
avatar_image_filename = None
for im_format, mime_type in image_formats.items():
if avatar_url.endswith('.' + im_format) or \
'.' + im_format + '?' in avatar_url:
session_headers = {
'Accept': 'image/' + mime_type
2020-12-12 14:23:14 +00:00
}
2022-01-04 14:33:19 +00:00
avatar_image_filename = avatar_image_path + '.' + im_format
2020-12-12 14:23:14 +00:00
2022-01-04 14:33:19 +00:00
if not avatar_image_filename:
2020-11-09 15:22:59 +00:00
return None
2022-01-04 14:33:19 +00:00
if (not os.path.isfile(avatar_image_filename) or force) and \
allow_downloads:
2020-11-09 15:22:59 +00:00
try:
2021-03-14 21:29:40 +00:00
if debug:
2022-01-04 14:33:19 +00:00
print('avatar image url: ' + avatar_url)
result = session.get(avatar_url,
headers=session_headers,
2022-04-24 20:33:07 +00:00
params=None,
2023-08-25 21:01:51 +00:00
allow_redirects=True)
2020-11-09 15:22:59 +00:00
if result.status_code < 200 or \
result.status_code > 202:
2021-03-14 21:29:40 +00:00
if debug:
print('Avatar image download failed with status ' +
str(result.status_code))
2020-11-09 15:22:59 +00:00
# remove partial download
2022-01-04 14:33:19 +00:00
if os.path.isfile(avatar_image_filename):
try:
2022-01-04 14:33:19 +00:00
os.remove(avatar_image_filename)
2021-11-25 18:42:38 +00:00
except OSError:
2021-12-29 21:55:09 +00:00
print('EX: ' +
'update_avatar_image_cache unable to delete ' +
2022-01-04 14:33:19 +00:00
avatar_image_filename)
2020-11-09 15:22:59 +00:00
else:
media_binary = result.content
if binary_is_image(avatar_image_filename, media_binary):
with open(avatar_image_filename, 'wb') as fp_av:
fp_av.write(media_binary)
if debug:
print('avatar image downloaded for ' + actor)
return avatar_image_filename.replace(base_dir +
'/cache', '')
else:
print('WARN: update_avatar_image_cache ' +
'avatar image binary not recognized ' +
actor + ' ' + str(media_binary[0:20]))
2024-02-07 10:04:27 +00:00
except BaseException as ex:
2021-11-01 17:12:17 +00:00
print('EX: Failed to download avatar image: ' +
2022-01-04 14:33:19 +00:00
str(avatar_url) + ' ' + str(ex))
2020-11-09 15:22:59 +00:00
prof = 'https://www.w3.org/ns/activitystreams'
if '/channel/' not in actor or '/accounts/' not in actor:
2022-01-04 14:33:19 +00:00
session_headers = {
2020-11-09 15:22:59 +00:00
'Accept': 'application/activity+json; profile="' + prof + '"'
}
else:
2022-01-04 14:33:19 +00:00
session_headers = {
2020-11-09 15:22:59 +00:00
'Accept': 'application/ld+json; profile="' + prof + '"'
}
2022-01-04 14:33:19 +00:00
person_json = \
2021-12-29 21:55:09 +00:00
get_json(signing_priv_key_pem, session, actor,
2022-01-04 14:33:19 +00:00
session_headers, None,
2021-12-29 21:55:09 +00:00
debug, __version__, http_prefix, None)
2023-08-13 09:58:02 +00:00
if get_json_valid(person_json):
2022-01-04 14:33:19 +00:00
if not person_json.get('id'):
2020-11-09 15:22:59 +00:00
return None
pub_key, _ = get_actor_public_key_from_id(person_json, None)
if not pub_key:
2020-11-09 15:22:59 +00:00
return None
2022-01-04 14:33:19 +00:00
if person_json['id'] != actor:
2020-11-09 15:22:59 +00:00
return None
2021-12-25 22:17:49 +00:00
if not person_cache.get(actor):
2020-11-09 15:22:59 +00:00
return None
cache_key, _ = \
get_actor_public_key_from_id(person_cache[actor]['actor'],
None)
if cache_key != pub_key:
2020-11-09 15:22:59 +00:00
print("ERROR: " +
"public keys don't match when downloading actor for " +
actor)
return None
2022-01-04 14:33:19 +00:00
store_person_in_cache(base_dir, actor, person_json, person_cache,
allow_downloads)
2022-06-12 12:30:14 +00:00
return get_person_avatar_url(base_dir, actor, person_cache)
2020-11-09 15:22:59 +00:00
return None
2022-01-04 14:33:19 +00:00
return avatar_image_filename.replace(base_dir + '/cache', '')
2020-11-09 15:22:59 +00:00
2021-12-29 21:55:09 +00:00
def scheduled_posts_exist(base_dir: str, nickname: str, domain: str) -> bool:
2020-11-09 15:22:59 +00:00
"""Returns true if there are posts scheduled to be delivered
"""
2022-01-04 14:33:19 +00:00
schedule_index_filename = \
2021-12-26 12:02:29 +00:00
acct_dir(base_dir, nickname, domain) + '/schedule.index'
2022-01-04 14:33:19 +00:00
if not os.path.isfile(schedule_index_filename):
2020-11-09 15:22:59 +00:00
return False
2022-06-10 13:01:39 +00:00
if text_in_file('#users#', schedule_index_filename):
2020-11-09 15:22:59 +00:00
return True
return False
2023-06-27 10:41:39 +00:00
def shares_timeline_json(actor: str, page_number: int, items_per_page: int,
2021-12-29 21:55:09 +00:00
base_dir: str, domain: str, nickname: str,
2022-01-04 14:33:19 +00:00
max_shares_per_account: int,
2021-12-29 21:55:09 +00:00
shared_items_federated_domains: [],
2022-01-04 14:33:19 +00:00
shares_file_type: str) -> ({}, bool):
2020-11-09 15:22:59 +00:00
"""Get a page on the shared items timeline as json
2022-01-04 14:33:19 +00:00
max_shares_per_account helps to avoid one person dominating the timeline
2020-11-09 15:22:59 +00:00
by sharing a large number of things
"""
2022-01-04 14:33:19 +00:00
all_shares_json = {}
for _, dirs, files in os.walk(base_dir + '/accounts'):
2020-11-09 15:22:59 +00:00
for handle in dirs:
2021-12-26 18:46:43 +00:00
if not is_account_dir(handle):
2020-12-06 14:42:42 +00:00
continue
2022-12-18 15:29:54 +00:00
account_dir = acct_handle_dir(base_dir, handle)
2022-01-04 14:33:19 +00:00
shares_filename = account_dir + '/' + shares_file_type + '.json'
if not os.path.isfile(shares_filename):
2020-12-06 14:42:42 +00:00
continue
2022-01-04 14:33:19 +00:00
shares_json = load_json(shares_filename)
if not shares_json:
2020-12-06 14:42:42 +00:00
continue
2022-01-04 14:33:19 +00:00
account_nickname = handle.split('@')[0]
# Don't include shared items from blocked accounts
2022-01-04 14:33:19 +00:00
if account_nickname != nickname:
2021-12-29 21:55:09 +00:00
if is_blocked(base_dir, nickname, domain,
account_nickname, domain, None, None):
continue
2020-12-06 14:42:42 +00:00
# actor who owns this share
2022-01-04 14:33:19 +00:00
owner = actor.split('/users/')[0] + '/users/' + account_nickname
2020-12-06 14:42:42 +00:00
ctr = 0
2022-01-04 14:33:19 +00:00
for item_id, item in shares_json.items():
2020-12-06 14:42:42 +00:00
# assign owner to the item
item['actor'] = owner
2022-01-04 14:33:19 +00:00
item['shareId'] = item_id
all_shares_json[str(item['published'])] = item
2020-12-06 14:42:42 +00:00
ctr += 1
2022-01-04 14:33:19 +00:00
if ctr >= max_shares_per_account:
2020-12-06 14:42:42 +00:00
break
2020-12-13 22:13:45 +00:00
break
2021-12-25 18:05:01 +00:00
if shared_items_federated_domains:
2022-01-04 14:33:19 +00:00
if shares_file_type == 'shares':
catalogs_dir = base_dir + '/cache/catalogs'
else:
2022-01-04 14:33:19 +00:00
catalogs_dir = base_dir + '/cache/wantedItems'
if os.path.isdir(catalogs_dir):
for _, dirs, files in os.walk(catalogs_dir):
for fname in files:
if '#' in fname:
continue
2022-01-04 14:33:19 +00:00
if not fname.endswith('.' + shares_file_type + '.json'):
continue
2022-01-04 14:33:19 +00:00
federated_domain = fname.split('.')[0]
if federated_domain not in shared_items_federated_domains:
continue
2022-01-04 14:33:19 +00:00
shares_filename = catalogs_dir + '/' + fname
shares_json = load_json(shares_filename)
if not shares_json:
continue
ctr = 0
2022-01-04 14:33:19 +00:00
for item_id, item in shares_json.items():
# assign owner to the item
2022-01-04 14:33:19 +00:00
if '--shareditems--' not in item_id:
continue
2022-01-04 14:33:19 +00:00
share_actor = item_id.split('--shareditems--')[0]
share_actor = share_actor.replace('___', '://')
share_actor = share_actor.replace('--', '/')
share_nickname = get_nickname_from_actor(share_actor)
if not share_nickname:
continue
2021-12-29 21:55:09 +00:00
if is_blocked(base_dir, nickname, domain,
share_nickname, federated_domain,
None, None):
continue
2022-01-04 14:33:19 +00:00
item['actor'] = share_actor
item['shareId'] = item_id
all_shares_json[str(item['published'])] = item
ctr += 1
2022-01-04 14:33:19 +00:00
if ctr >= max_shares_per_account:
break
break
2020-11-09 15:22:59 +00:00
# sort the shared items in descending order of publication date
2022-01-04 14:33:19 +00:00
shares_json = OrderedDict(sorted(all_shares_json.items(), reverse=True))
last_page = False
2023-06-27 10:41:39 +00:00
start_index = items_per_page * page_number
2022-01-04 14:33:19 +00:00
max_index = len(shares_json.items())
if max_index < items_per_page:
last_page = True
if start_index >= max_index - items_per_page:
last_page = True
start_index = max_index - items_per_page
2022-05-30 20:47:23 +00:00
start_index = max(start_index, 0)
2020-11-09 15:22:59 +00:00
ctr = 0
2022-01-04 14:33:19 +00:00
result_json = {}
for published, item in shares_json.items():
if ctr >= start_index + items_per_page:
2020-11-09 15:22:59 +00:00
break
2022-01-04 14:33:19 +00:00
if ctr < start_index:
2020-11-09 15:22:59 +00:00
ctr += 1
continue
2022-01-04 14:33:19 +00:00
result_json[published] = item
2020-11-09 15:22:59 +00:00
ctr += 1
2022-01-04 14:33:19 +00:00
return result_json, last_page
2020-11-09 15:22:59 +00:00
2023-06-27 16:41:33 +00:00
def get_shares_collection(actor: str, page_number: int, items_per_page: int,
base_dir: str, domain: str, nickname: str,
max_shares_per_account: int,
shared_items_federated_domains: [],
shares_file_type: str) -> {}:
"""Returns an ActivityStreams collection of ValueFlows Proposal objects
https://codeberg.org/fediverse/fep/src/branch/main/fep/0837/fep-0837.md
2023-06-27 16:41:33 +00:00
"""
shares_collection = []
shares_json, _ = \
shares_timeline_json(actor, page_number, items_per_page,
base_dir, domain, nickname,
max_shares_per_account,
shared_items_federated_domains, shares_file_type)
if shares_file_type == 'shares':
share_type = 'offer'
collection_name = nickname + "'s Shared Items"
2023-06-27 16:41:33 +00:00
else:
share_type = 'request'
collection_name = nickname + "'s Wanted Items"
2023-06-27 16:41:33 +00:00
2023-08-23 12:24:25 +00:00
for share_id, shared_item in shares_json.items():
shared_item['shareId'] = share_id
2023-08-23 12:36:23 +00:00
shared_item['actor'] = actor
offer_item = vf_proposal_from_share(shared_item, share_type)
if offer_item:
shares_collection.append(offer_item)
2023-07-05 17:49:34 +00:00
result_json = {
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"id": actor + '?page=' + str(page_number),
"type": "OrderedCollection",
"name": collection_name,
2023-07-05 17:49:34 +00:00
"orderedItems": shares_collection
}
return result_json
2023-06-27 16:41:33 +00:00
2021-12-29 21:55:09 +00:00
def post_contains_public(post_json_object: {}) -> bool:
2020-11-09 15:22:59 +00:00
"""Does the given post contain #Public
"""
2022-01-04 14:33:19 +00:00
contains_public = False
2021-12-25 22:09:19 +00:00
if not post_json_object['object'].get('to'):
2022-01-04 14:33:19 +00:00
return contains_public
2020-11-09 15:22:59 +00:00
2022-01-04 14:33:19 +00:00
for to_address in post_json_object['object']['to']:
2023-02-09 20:40:42 +00:00
if to_address.endswith('#Public') or \
to_address == 'as:Public' or \
to_address == 'Public':
2022-01-04 14:33:19 +00:00
contains_public = True
2020-11-09 15:22:59 +00:00
break
2022-01-04 14:33:19 +00:00
if not contains_public:
2021-12-25 22:09:19 +00:00
if post_json_object['object'].get('cc'):
2022-01-04 14:33:19 +00:00
for to_address2 in post_json_object['object']['cc']:
2023-02-09 20:40:42 +00:00
if to_address2.endswith('#Public') or \
to_address2 == 'as:Public' or \
to_address2 == 'Public':
2022-01-04 14:33:19 +00:00
contains_public = True
2020-11-09 15:22:59 +00:00
break
2022-01-04 14:33:19 +00:00
return contains_public
2020-11-09 15:22:59 +00:00
2021-12-29 21:55:09 +00:00
def _get_image_file(base_dir: str, name: str, directory: str,
2022-06-01 17:45:59 +00:00
theme: str) -> (str, str):
2020-11-09 15:22:59 +00:00
"""
2020-11-09 15:40:24 +00:00
returns the filenames for an image with the given name
"""
2022-01-04 14:33:19 +00:00
banner_extensions = get_image_extensions()
2021-12-31 21:18:12 +00:00
banner_file = ''
banner_filename = ''
2022-08-20 13:37:45 +00:00
im_name = name
2022-01-04 14:33:19 +00:00
for ext in banner_extensions:
2022-08-20 13:37:45 +00:00
banner_file_test = im_name + '.' + ext
2022-01-04 14:33:19 +00:00
banner_filename_test = directory + '/' + banner_file_test
if os.path.isfile(banner_filename_test):
2022-08-20 13:37:45 +00:00
banner_file = banner_file_test
2022-01-04 14:33:19 +00:00
banner_filename = banner_filename_test
2021-12-31 21:18:12 +00:00
return banner_file, banner_filename
2021-08-21 13:03:28 +00:00
# if not found then use the default image
theme = 'default'
2021-12-25 16:17:53 +00:00
directory = base_dir + '/theme/' + theme
2022-01-04 14:33:19 +00:00
for ext in banner_extensions:
banner_file_test = name + '.' + ext
banner_filename_test = directory + '/' + banner_file_test
if os.path.isfile(banner_filename_test):
2021-12-31 21:18:12 +00:00
banner_file = name + '_' + theme + '.' + ext
2022-01-04 14:33:19 +00:00
banner_filename = banner_filename_test
2021-08-21 13:03:28 +00:00
break
2021-12-31 21:18:12 +00:00
return banner_file, banner_filename
2020-11-09 15:40:24 +00:00
2021-12-29 21:55:09 +00:00
def get_banner_file(base_dir: str,
nickname: str, domain: str, theme: str) -> (str, str):
2022-01-04 14:33:19 +00:00
"""Gets the image for the timeline banner
"""
account_dir = acct_dir(base_dir, nickname, domain)
2022-06-01 17:45:59 +00:00
return _get_image_file(base_dir, 'banner', account_dir, theme)
2020-11-09 15:40:24 +00:00
def get_profile_background_file(base_dir: str,
nickname: str, domain: str,
theme: str) -> (str, str):
"""Gets the image for the profile background
"""
account_dir = acct_dir(base_dir, nickname, domain)
return _get_image_file(base_dir, 'image', account_dir, theme)
2021-12-29 21:55:09 +00:00
def get_search_banner_file(base_dir: str,
nickname: str, domain: str,
theme: str) -> (str, str):
2022-01-04 14:33:19 +00:00
"""Gets the image for the search banner
"""
account_dir = acct_dir(base_dir, nickname, domain)
2022-06-01 17:45:59 +00:00
return _get_image_file(base_dir, 'search_banner', account_dir, theme)
2020-11-09 15:40:24 +00:00
2021-12-29 21:55:09 +00:00
def get_left_image_file(base_dir: str,
nickname: str, domain: str, theme: str) -> (str, str):
2022-01-04 14:33:19 +00:00
"""Gets the image for the left column
"""
account_dir = acct_dir(base_dir, nickname, domain)
2022-06-01 17:45:59 +00:00
return _get_image_file(base_dir, 'left_col_image', account_dir, theme)
2020-11-09 15:40:24 +00:00
2021-12-29 21:55:09 +00:00
def get_right_image_file(base_dir: str,
nickname: str, domain: str, theme: str) -> (str, str):
2022-01-04 14:33:19 +00:00
"""Gets the image for the right column
"""
account_dir = acct_dir(base_dir, nickname, domain)
2022-06-01 17:45:59 +00:00
return _get_image_file(base_dir, 'right_col_image', account_dir, theme)
2020-11-09 19:41:01 +00:00
2022-01-04 14:33:19 +00:00
def html_header_with_external_style(css_filename: str, instance_title: str,
2021-12-29 21:55:09 +00:00
metadata: str, lang='en') -> str:
if metadata is None:
metadata = ''
2022-01-04 14:33:19 +00:00
css_file = '/' + css_filename.split('/')[-1]
pwa_theme_color, pwa_theme_background_color = \
get_pwa_theme_colors(css_filename)
2022-01-04 14:33:19 +00:00
html_str = \
2021-07-06 12:50:38 +00:00
'<!DOCTYPE html>\n' + \
2022-02-28 23:08:06 +00:00
'<!--\n' + \
'Thankyou for using Epicyon. If you are reading this message then ' + \
'consider joining the development at ' + \
'https://gitlab.com/bashrc2/epicyon\n' + \
'-->\n' + \
2021-07-06 12:50:38 +00:00
'<html lang="' + lang + '">\n' + \
' <head>\n' + \
' <meta charset="utf-8">\n' + \
2021-11-08 10:06:32 +00:00
' <link rel="stylesheet" media="all" ' + \
2022-01-04 14:33:19 +00:00
'href="' + css_file + '">\n' + \
2021-07-06 12:50:38 +00:00
' <link rel="manifest" href="/manifest.json">\n' + \
2021-11-08 10:06:32 +00:00
' <link href="/favicon.ico" rel="icon" type="image/x-icon">\n' + \
' <meta content="/browserconfig.xml" ' + \
'name="msapplication-config">\n' + \
' <meta content="yes" name="apple-mobile-web-app-capable">\n' + \
' <link href="/apple-touch-icon.png" rel="apple-touch-icon" ' + \
'sizes="180x180">\n' + \
' <meta name="theme-color" content="' + pwa_theme_color + '">\n' + \
metadata + \
' <meta name="apple-mobile-web-app-status-bar-style" ' + \
2022-02-27 12:46:19 +00:00
'content="' + pwa_theme_background_color + '">\n' + \
2022-01-04 14:33:19 +00:00
' <title>' + instance_title + '</title>\n' + \
2021-07-06 12:50:38 +00:00
' </head>\n' + \
' <body>\n'
2022-01-04 14:33:19 +00:00
return html_str
2020-11-09 19:41:01 +00:00
2022-01-04 14:33:19 +00:00
def html_header_with_person_markup(css_filename: str, instance_title: str,
2021-12-29 21:55:09 +00:00
actor_json: {}, city: str,
content_license_url: str,
lang='en') -> str:
"""html header which includes person markup
https://schema.org/Person
"""
2021-12-26 10:29:52 +00:00
if not actor_json:
2022-01-04 14:33:19 +00:00
html_str = \
2021-12-31 21:18:12 +00:00
html_header_with_external_style(css_filename,
2022-01-04 14:33:19 +00:00
instance_title, None, lang)
return html_str
2022-01-04 14:33:19 +00:00
city_markup = ''
2021-05-17 10:46:31 +00:00
if city:
city = city.lower().title()
2022-01-04 14:33:19 +00:00
add_comma = ''
country_markup = ''
2021-05-17 10:46:31 +00:00
if ',' in city:
country = city.split(',', 1)[1].strip().title()
city = city.split(',', 1)[0]
2022-01-04 14:33:19 +00:00
country_markup = \
2021-11-07 12:35:52 +00:00
' "addressCountry": "' + country + '"\n'
2022-01-04 14:33:19 +00:00
add_comma = ','
city_markup = \
2021-11-07 12:31:47 +00:00
' "address": {\n' + \
' "@type": "PostalAddress",\n' + \
2022-01-04 14:33:19 +00:00
' "addressLocality": "' + city + '"' + \
add_comma + '\n' + country_markup + ' },\n'
2021-05-17 10:46:31 +00:00
2022-01-04 14:33:19 +00:00
skills_markup = ''
2021-12-26 10:29:52 +00:00
if actor_json.get('hasOccupation'):
if isinstance(actor_json['hasOccupation'], list):
2022-01-04 14:33:19 +00:00
skills_markup = ' "hasOccupation": [\n'
first_entry = True
for skill_dict in actor_json['hasOccupation']:
if skill_dict['@type'] == 'Role':
if not first_entry:
skills_markup += ',\n'
skl = skill_dict['hasOccupation']
role_name = skl['name']
if not role_name:
role_name = 'member'
2021-05-16 16:07:02 +00:00
category = \
2022-01-04 14:33:19 +00:00
skl['occupationalCategory']['codeValue']
category_url = \
2021-05-16 16:25:16 +00:00
'https://www.onetonline.org/link/summary/' + category
2022-01-04 14:33:19 +00:00
skills_markup += \
2021-07-06 12:50:38 +00:00
' {\n' + \
' "@type": "Role",\n' + \
' "hasOccupation": {\n' + \
' "@type": "Occupation",\n' + \
2022-01-04 14:33:19 +00:00
' "name": "' + role_name + '",\n' + \
2021-07-06 12:50:38 +00:00
' "description": ' + \
'"Fediverse instance role",\n' + \
' "occupationLocation": {\n' + \
' "@type": "City",\n' + \
' "name": "' + city + '"\n' + \
' },\n' + \
' "occupationalCategory": {\n' + \
' "@type": "CategoryCode",\n' + \
' "inCodeSet": {\n' + \
' "@type": "CategoryCodeSet",\n' + \
' "name": "O*Net-SOC",\n' + \
' "dateModified": "2019",\n' + \
2021-05-16 16:20:38 +00:00
' ' + \
2021-07-06 12:50:38 +00:00
'"url": "https://www.onetonline.org/"\n' + \
' },\n' + \
' "codeValue": "' + category + '",\n' + \
2022-01-04 14:33:19 +00:00
' "url": "' + category_url + '"\n' + \
2021-07-06 12:50:38 +00:00
' }\n' + \
' }\n' + \
' }'
2022-01-04 14:33:19 +00:00
elif skill_dict['@type'] == 'Occupation':
if not first_entry:
skills_markup += ',\n'
oc_name = skill_dict['name']
if not oc_name:
oc_name = 'member'
skills_list = skill_dict['skills']
skills_list_str = '['
for skill_str in skills_list:
if skills_list_str != '[':
skills_list_str += ', '
skills_list_str += '"' + skill_str + '"'
skills_list_str += ']'
skills_markup += \
2021-07-06 12:50:38 +00:00
' {\n' + \
' "@type": "Occupation",\n' + \
2022-01-04 14:33:19 +00:00
' "name": "' + oc_name + '",\n' + \
2021-07-06 12:50:38 +00:00
' "description": ' + \
'"Fediverse instance occupation",\n' + \
' "occupationLocation": {\n' + \
' "@type": "City",\n' + \
' "name": "' + city + '"\n' + \
' },\n' + \
2022-01-04 14:33:19 +00:00
' "skills": ' + skills_list_str + '\n' + \
2021-07-06 12:50:38 +00:00
' }'
2022-01-04 14:33:19 +00:00
first_entry = False
skills_markup += '\n ],\n'
2021-05-16 16:07:02 +00:00
2021-12-27 15:43:22 +00:00
description = remove_html(actor_json['summary'])
2022-01-04 14:33:19 +00:00
name_str = remove_html(actor_json['name'])
2021-12-26 10:29:52 +00:00
domain_full = actor_json['id'].split('://')[1].split('/')[0]
handle = actor_json['preferredUsername'] + '@' + domain_full
2021-11-07 12:27:52 +00:00
2023-12-09 14:18:24 +00:00
url_str = get_url_from_post(actor_json['icon']['url'])
icon_url = remove_html(url_str)
2022-01-04 14:33:19 +00:00
person_markup = \
2021-11-07 12:27:52 +00:00
' "about": {\n' + \
' "@type" : "Person",\n' + \
2022-01-04 14:33:19 +00:00
' "name": "' + name_str + '",\n' + \
' "image": "' + icon_url + '",\n' + \
2021-11-07 12:27:52 +00:00
' "description": "' + description + '",\n' + \
2022-01-04 14:33:19 +00:00
city_markup + skills_markup + \
2021-12-26 10:29:52 +00:00
' "url": "' + actor_json['id'] + '"\n' + \
2021-11-07 12:27:52 +00:00
' },\n'
2022-01-04 14:33:19 +00:00
profile_markup = \
2021-11-07 11:57:58 +00:00
' <script id="initial-state" type="application/ld+json">\n' + \
' {\n' + \
2021-11-07 12:27:52 +00:00
' "@context":"https://schema.org",\n' + \
' "@type": "ProfilePage",\n' + \
' "mainEntityOfPage": {\n' + \
' "@type": "WebPage",\n' + \
2021-12-26 10:29:52 +00:00
" \"@id\": \"" + actor_json['id'] + "\"\n" + \
2022-01-04 14:33:19 +00:00
' },\n' + person_markup + \
2021-11-07 12:27:52 +00:00
' "accountablePerson": {\n' + \
' "@type": "Person",\n' + \
2022-01-04 14:33:19 +00:00
' "name": "' + name_str + '"\n' + \
2021-11-07 12:27:52 +00:00
' },\n' + \
' "copyrightHolder": {\n' + \
' "@type": "Person",\n' + \
2022-01-04 14:33:19 +00:00
' "name": "' + name_str + '"\n' + \
2021-11-07 12:27:52 +00:00
' },\n' + \
2022-01-04 14:33:19 +00:00
' "name": "' + name_str + '",\n' + \
' "image": "' + icon_url + '",\n' + \
2021-05-16 11:16:50 +00:00
' "description": "' + description + '",\n' + \
2021-12-25 17:13:38 +00:00
' "license": "' + content_license_url + '"\n' + \
' }\n' + \
' </script>\n'
2021-11-07 11:32:08 +00:00
2021-12-27 15:43:22 +00:00
description = remove_html(description)
2023-12-09 14:18:24 +00:00
url_str = get_url_from_post(actor_json['url'])
actor2_url = remove_html(url_str)
2022-01-04 14:33:19 +00:00
og_metadata = \
2021-11-07 11:32:08 +00:00
" <meta content=\"profile\" property=\"og:type\" />\n" + \
" <meta content=\"" + description + \
"\" name='description'>\n" + \
" <meta content=\"" + actor2_url + \
2021-11-07 11:34:20 +00:00
"\" property=\"og:url\" />\n" + \
2021-12-26 10:00:46 +00:00
" <meta content=\"" + domain_full + \
2021-11-07 11:34:20 +00:00
"\" property=\"og:site_name\" />\n" + \
2022-01-04 14:33:19 +00:00
" <meta content=\"" + name_str + " (@" + handle + \
2021-11-07 11:34:20 +00:00
")\" property=\"og:title\" />\n" + \
2021-11-07 11:32:08 +00:00
" <meta content=\"" + description + \
2021-11-07 11:34:20 +00:00
"\" property=\"og:description\" />\n" + \
" <meta content=\"" + icon_url + \
2021-11-07 11:34:20 +00:00
"\" property=\"og:image\" />\n" + \
2021-11-07 11:32:08 +00:00
" <meta content=\"400\" property=\"og:image:width\" />\n" + \
2021-11-07 11:34:20 +00:00
" <meta content=\"400\" property=\"og:image:height\" />\n" + \
" <meta content=\"summary\" property=\"twitter:card\" />\n" + \
" <meta content=\"" + handle + \
"\" property=\"profile:username\" />\n"
2021-12-26 10:29:52 +00:00
if actor_json.get('attachment'):
2022-01-04 14:33:19 +00:00
og_tags = (
'email', 'openpgp', 'blog', 'xmpp', 'matrix', 'briar',
'cwtch', 'languages'
)
2022-01-04 14:33:19 +00:00
for attach_json in actor_json['attachment']:
if not attach_json.get('name'):
if not attach_json.get('schema:name'):
continue
prop_value_name, _ = get_attachment_property_value(attach_json)
if not prop_value_name:
continue
if attach_json.get('name'):
name = attach_json['name'].lower()
else:
name = attach_json['schema:name'].lower()
value = attach_json[prop_value_name]
2022-01-04 14:33:19 +00:00
for og_tag in og_tags:
if name != og_tag:
continue
2022-01-04 14:33:19 +00:00
og_metadata += \
" <meta content=\"" + value + \
2022-01-04 14:33:19 +00:00
"\" property=\"og:" + og_tag + "\" />\n"
2021-11-07 11:32:08 +00:00
2022-01-04 14:33:19 +00:00
html_str = \
html_header_with_external_style(css_filename, instance_title,
og_metadata + profile_markup, lang)
return html_str
2022-01-04 14:33:19 +00:00
def html_header_with_website_markup(css_filename: str, instance_title: str,
2021-12-29 21:55:09 +00:00
http_prefix: str, domain: str,
system_language: str) -> str:
2021-05-14 11:27:08 +00:00
"""html header which includes website markup
https://schema.org/WebSite
"""
2022-01-04 14:33:19 +00:00
license_url = 'https://www.gnu.org/licenses/agpl-3.0.rdf'
2021-05-15 09:08:01 +00:00
# social networking category
2022-01-04 14:33:19 +00:00
genre_url = 'http://vocab.getty.edu/aat/300312270'
2021-05-15 09:08:01 +00:00
2022-01-04 14:33:19 +00:00
website_markup = \
2021-11-07 11:57:58 +00:00
' <script id="initial-state" type="application/ld+json">\n' + \
2021-05-14 11:27:08 +00:00
' {\n' + \
' "@context" : "http://schema.org",\n' + \
' "@type" : "WebSite",\n' + \
2022-01-04 14:33:19 +00:00
' "name": "' + instance_title + '",\n' + \
2021-12-25 17:09:22 +00:00
' "url": "' + http_prefix + '://' + domain + '",\n' + \
2022-01-04 14:33:19 +00:00
' "license": "' + license_url + '",\n' + \
2021-12-25 23:03:28 +00:00
' "inLanguage": "' + system_language + '",\n' + \
2021-05-14 11:27:08 +00:00
' "isAccessibleForFree": true,\n' + \
2022-01-04 14:33:19 +00:00
' "genre": "' + genre_url + '",\n' + \
2021-05-14 11:27:08 +00:00
' "accessMode": ["textual", "visual"],\n' + \
' "accessModeSufficient": ["textual"],\n' + \
2021-05-14 11:30:05 +00:00
' "accessibilityAPI" : ["ARIA"],\n' + \
2021-05-14 11:27:08 +00:00
' "accessibilityControl" : [\n' + \
' "fullKeyboardControl",\n' + \
' "fullTouchControl",\n' + \
' "fullMouseControl"\n' + \
' ],\n' + \
' "encodingFormat" : [\n' + \
' "text/html", "image/png", "image/webp",\n' + \
' "image/jpeg", "image/gif", "text/css"\n' + \
2021-05-14 11:29:20 +00:00
' ]\n' + \
2021-05-14 11:27:08 +00:00
' }\n' + \
' </script>\n'
2021-11-07 23:26:40 +00:00
2022-01-04 14:33:19 +00:00
og_metadata = \
2021-11-07 23:26:40 +00:00
' <meta content="Epicyon hosted on ' + domain + \
'" property="og:site_name" />\n' + \
2021-12-25 17:09:22 +00:00
' <meta content="' + http_prefix + '://' + domain + \
2021-11-07 23:26:40 +00:00
'/about" property="og:url" />\n' + \
' <meta content="website" property="og:type" />\n' + \
2022-01-04 14:33:19 +00:00
' <meta content="' + instance_title + \
2021-11-07 23:26:40 +00:00
'" property="og:title" />\n' + \
2021-12-25 17:09:22 +00:00
' <meta content="' + http_prefix + '://' + domain + \
2021-11-07 23:26:40 +00:00
'/logo.png" property="og:image" />\n' + \
2021-12-25 23:03:28 +00:00
' <meta content="' + system_language + \
2021-11-07 23:26:40 +00:00
'" property="og:locale" />\n' + \
' <meta content="summary_large_image" property="twitter:card" />\n'
2022-01-04 14:33:19 +00:00
html_str = \
html_header_with_external_style(css_filename, instance_title,
og_metadata + website_markup,
2021-12-29 21:55:09 +00:00
system_language)
2022-01-04 14:33:19 +00:00
return html_str
2021-05-14 11:27:08 +00:00
2022-01-04 14:33:19 +00:00
def html_header_with_blog_markup(css_filename: str, instance_title: str,
2021-12-29 21:55:09 +00:00
http_prefix: str, domain: str, nickname: str,
system_language: str,
published: str, modified: str,
2024-02-07 10:12:03 +00:00
title: str, snippet: str, url: str,
2021-12-29 21:55:09 +00:00
content_license_url: str) -> str:
2021-05-15 19:39:34 +00:00
"""html header which includes blog post markup
https://schema.org/BlogPosting
"""
2022-01-04 14:33:19 +00:00
author_url = local_actor_url(http_prefix, nickname, domain)
about_url = http_prefix + '://' + domain + '/about.html'
2021-05-17 14:25:46 +00:00
# license for content on the site may be different from
# the software license
2022-01-04 14:33:19 +00:00
blog_markup = \
2021-11-07 11:57:58 +00:00
' <script id="initial-state" type="application/ld+json">\n' + \
2021-05-15 19:39:34 +00:00
' {\n' + \
' "@context" : "http://schema.org",\n' + \
' "@type" : "BlogPosting",\n' + \
' "headline": "' + title + '",\n' + \
' "datePublished": "' + published + '",\n' + \
2021-11-08 13:20:06 +00:00
' "dateModified": "' + modified + '",\n' + \
2021-05-15 19:39:34 +00:00
' "author": {\n' + \
' "@type": "Person",\n' + \
' "name": "' + nickname + '",\n' + \
2022-01-04 14:33:19 +00:00
' "sameAs": "' + author_url + '"\n' + \
2021-05-15 19:39:34 +00:00
' },\n' + \
' "publisher": {\n' + \
' "@type": "WebSite",\n' + \
2022-01-04 14:33:19 +00:00
' "name": "' + instance_title + '",\n' + \
' "sameAs": "' + about_url + '"\n' + \
2021-05-15 19:39:34 +00:00
' },\n' + \
2021-12-25 17:13:38 +00:00
' "license": "' + content_license_url + '",\n' + \
2021-05-15 19:39:34 +00:00
' "description": "' + snippet + '"\n' + \
' }\n' + \
' </script>\n'
2021-11-08 13:20:06 +00:00
2022-01-04 14:33:19 +00:00
og_metadata = \
2021-11-08 13:20:06 +00:00
' <meta property="og:locale" content="' + \
2021-12-25 23:03:28 +00:00
system_language + '" />\n' + \
2021-11-08 13:20:06 +00:00
' <meta property="og:type" content="article" />\n' + \
' <meta property="og:title" content="' + title + '" />\n' + \
' <meta property="og:url" content="' + url + '" />\n' + \
' <meta content="Epicyon hosted on ' + domain + \
'" property="og:site_name" />\n' + \
' <meta property="article:published_time" content="' + \
published + '" />\n' + \
' <meta property="article:modified_time" content="' + \
2021-11-08 13:23:13 +00:00
modified + '" />\n'
2021-11-08 13:20:06 +00:00
2022-01-04 14:33:19 +00:00
html_str = \
html_header_with_external_style(css_filename, instance_title,
og_metadata + blog_markup,
2021-12-29 21:55:09 +00:00
system_language)
2022-01-04 14:33:19 +00:00
return html_str
2021-05-15 19:39:34 +00:00
2021-12-29 21:55:09 +00:00
def html_footer() -> str:
2022-01-04 14:33:19 +00:00
html_str = ' </body>\n'
html_str += '</html>\n'
return html_str
2020-11-09 19:41:01 +00:00
2021-12-29 21:55:09 +00:00
def load_individual_post_as_html_from_cache(base_dir: str,
nickname: str, domain: str,
post_json_object: {}) -> str:
2020-11-09 19:41:01 +00:00
"""If a cached html version of the given post exists then load it and
return the html text
This is much quicker than generating the html from the json object
"""
2022-01-04 14:33:19 +00:00
cached_post_filename = \
2021-12-26 23:41:34 +00:00
get_cached_post_filename(base_dir, nickname, domain, post_json_object)
2020-11-09 19:41:01 +00:00
2022-01-04 14:33:19 +00:00
post_html = ''
if not cached_post_filename:
return post_html
2020-11-09 19:41:01 +00:00
2022-01-04 14:33:19 +00:00
if not os.path.isfile(cached_post_filename):
return post_html
2020-11-09 19:41:01 +00:00
tries = 0
while tries < 3:
try:
2022-06-09 14:46:30 +00:00
with open(cached_post_filename, 'r', encoding='utf-8') as file:
2022-01-04 14:33:19 +00:00
post_html = file.read()
2020-11-09 19:41:01 +00:00
break
2023-02-09 12:41:31 +00:00
except OSError as ex:
2021-12-29 21:55:09 +00:00
print('ERROR: load_individual_post_as_html_from_cache ' +
2021-12-25 15:28:52 +00:00
str(tries) + ' ' + str(ex))
2020-11-09 19:41:01 +00:00
# no sleep
tries += 1
2022-01-04 14:33:19 +00:00
if post_html:
return post_html
2020-11-09 19:41:01 +00:00
2021-12-29 21:55:09 +00:00
def add_emoji_to_display_name(session, base_dir: str, http_prefix: str,
nickname: str, domain: str,
2022-07-18 16:18:04 +00:00
display_name: str, in_profile_name: bool,
translate: {}) -> str:
2020-12-29 09:52:52 +00:00
"""Adds emoji icons to display names or CW on individual posts
2020-11-09 19:41:01 +00:00
"""
2022-01-04 14:33:19 +00:00
if ':' not in display_name:
return display_name
2020-11-09 19:41:01 +00:00
2022-01-04 14:33:19 +00:00
display_name = display_name.replace('<p>', '').replace('</p>', '')
emoji_tags = {}
# print('TAG: display_name before tags: ' + display_name)
display_name = \
2021-12-29 21:55:09 +00:00
add_html_tags(base_dir, http_prefix,
2022-07-18 16:18:04 +00:00
nickname, domain, display_name, [],
emoji_tags, translate)
2022-01-04 14:33:19 +00:00
display_name = display_name.replace('<p>', '').replace('</p>', '')
# print('TAG: display_name after tags: ' + display_name)
2020-11-09 19:41:01 +00:00
# convert the emoji dictionary to a list
2022-01-04 14:33:19 +00:00
emoji_tags_list = []
for _, tag in emoji_tags.items():
emoji_tags_list.append(tag)
# print('TAG: emoji tags list: ' + str(emoji_tags_list))
if not in_profile_name:
display_name = \
2021-12-29 21:55:09 +00:00
replace_emoji_from_tags(session, base_dir,
2022-01-04 14:33:19 +00:00
display_name, emoji_tags_list,
2022-04-21 13:03:40 +00:00
'post header', False, False)
2020-11-09 19:41:01 +00:00
else:
2022-01-04 14:33:19 +00:00
display_name = \
2021-12-29 21:55:09 +00:00
replace_emoji_from_tags(session, base_dir,
2022-01-04 14:33:19 +00:00
display_name, emoji_tags_list, 'profile',
2022-04-21 13:03:40 +00:00
False, False)
2022-01-04 14:33:19 +00:00
# print('TAG: display_name after tags 2: ' + display_name)
2020-11-09 19:41:01 +00:00
# remove any stray emoji
2022-01-04 14:33:19 +00:00
while ':' in display_name:
if '://' in display_name:
2020-11-09 19:41:01 +00:00
break
2022-01-04 14:33:19 +00:00
emoji_str = display_name.split(':')[1]
prev_display_name = display_name
display_name = display_name.replace(':' + emoji_str + ':', '').strip()
if prev_display_name == display_name:
2020-11-09 19:41:01 +00:00
break
2022-01-04 14:33:19 +00:00
# print('TAG: display_name after tags 3: ' + display_name)
# print('TAG: display_name after tag replacements: ' + display_name)
2020-11-09 19:41:01 +00:00
2022-01-04 14:33:19 +00:00
return display_name
2020-11-09 19:41:01 +00:00
2022-01-04 14:33:19 +00:00
def _is_image_mime_type(mime_type: str) -> bool:
2021-03-07 10:15:17 +00:00
"""Is the given mime type an image?
"""
2022-01-04 14:33:19 +00:00
if mime_type == 'image/svg+xml':
2021-08-03 09:09:04 +00:00
return True
2022-01-04 14:33:19 +00:00
if not mime_type.startswith('image/'):
2021-08-03 09:09:04 +00:00
return False
2021-12-26 14:26:16 +00:00
extensions = get_image_extensions()
2022-01-04 14:33:19 +00:00
ext = mime_type.split('/')[1]
2021-08-03 09:09:04 +00:00
if ext in extensions:
2021-03-07 10:15:17 +00:00
return True
return False
2022-01-04 14:33:19 +00:00
def _is_video_mime_type(mime_type: str) -> bool:
2021-03-07 10:15:17 +00:00
"""Is the given mime type a video?
"""
2022-01-04 14:33:19 +00:00
if not mime_type.startswith('video/'):
2021-08-03 09:09:04 +00:00
return False
2021-12-26 14:20:09 +00:00
extensions = get_video_extensions()
2022-01-04 14:33:19 +00:00
ext = mime_type.split('/')[1]
2021-08-03 09:09:04 +00:00
if ext in extensions:
2021-03-07 10:15:17 +00:00
return True
return False
2022-01-04 14:33:19 +00:00
def _is_audio_mime_type(mime_type: str) -> bool:
2021-03-07 10:15:17 +00:00
"""Is the given mime type an audio file?
"""
2022-01-04 14:33:19 +00:00
if mime_type == 'audio/mpeg':
2021-08-03 09:09:04 +00:00
return True
2022-01-04 14:33:19 +00:00
if not mime_type.startswith('audio/'):
2021-08-03 09:09:04 +00:00
return False
2021-12-26 14:24:03 +00:00
extensions = get_audio_extensions()
2022-01-04 14:33:19 +00:00
ext = mime_type.split('/')[1]
2021-08-03 09:09:04 +00:00
if ext in extensions:
2021-03-07 10:15:17 +00:00
return True
return False
2022-01-04 14:33:19 +00:00
def _is_attached_image(attachment_filename: str) -> bool:
2021-03-07 10:15:17 +00:00
"""Is the given attachment filename an image?
"""
2022-01-04 14:33:19 +00:00
if '.' not in attachment_filename:
2021-03-07 10:15:17 +00:00
return False
2024-02-05 20:05:00 +00:00
image_ext = get_image_extensions()
2022-01-04 14:33:19 +00:00
ext = attachment_filename.split('.')[-1]
if ext in image_ext:
2021-03-07 10:15:17 +00:00
return True
return False
2022-01-04 14:33:19 +00:00
def _is_attached_video(attachment_filename: str) -> bool:
2021-03-07 10:24:27 +00:00
"""Is the given attachment filename a video?
"""
2022-01-04 14:33:19 +00:00
if '.' not in attachment_filename:
2021-03-07 10:24:27 +00:00
return False
2022-01-04 14:33:19 +00:00
video_ext = (
2021-03-07 10:24:27 +00:00
'mp4', 'webm', 'ogv'
)
2022-01-04 14:33:19 +00:00
ext = attachment_filename.split('.')[-1]
if ext in video_ext:
2021-03-07 10:24:27 +00:00
return True
return False
def _is_nsfw(content: str) -> bool:
"""Does the given content indicate nsfw?
"""
content_lower = content.lower()
nsfw_tags = (
'nsfw', 'porn', 'pr0n', 'explicit', 'lewd',
2022-06-21 08:31:11 +00:00
'nude', 'boob', 'erotic', 'sex'
)
for tag_name in nsfw_tags:
if tag_name in content_lower:
return True
return False
def get_post_attachments_as_html(base_dir: str,
nickname: str, domain: str,
domain_full: str,
post_json_object: {}, box_name: str,
2021-12-29 21:55:09 +00:00
translate: {},
2022-01-04 14:33:19 +00:00
is_muted: bool, avatar_link: str,
reply_str: str, announce_str: str,
like_str: str,
bookmark_str: str, delete_str: str,
mute_str: str,
content: str,
minimize_all_images: bool,
system_language: str) -> (str, str):
2020-11-09 19:41:01 +00:00
"""Returns a string representing any attachments
"""
2022-01-04 14:33:19 +00:00
attachment_str = ''
attachment_ctr = 0
2022-01-04 14:33:19 +00:00
gallery_str = ''
attachment_dict = []
# handle peertube-style video posts, where the media links
# are stored in the url field
if post_json_object.get('object'):
media_type, media_url, _, _ = \
get_media_url_from_video(post_json_object['object'])
else:
media_type, media_url, _, _ = \
get_media_url_from_video(post_json_object)
if media_url and media_type:
attachment_dict = [{
'mediaType': media_type,
'name': content,
'type': 'Document',
'url': media_url
}]
if not post_json_object['object'].get('attachment'):
post_json_object['object']['attachment'] = \
attachment_dict
2021-12-25 22:09:19 +00:00
if not post_json_object['object'].get('attachment'):
2022-01-04 14:33:19 +00:00
return attachment_str, gallery_str
2020-11-09 19:41:01 +00:00
2021-12-25 22:09:19 +00:00
if not isinstance(post_json_object['object']['attachment'], list):
2022-01-04 14:33:19 +00:00
return attachment_str, gallery_str
2020-11-09 19:41:01 +00:00
attachment_dict += post_json_object['object']['attachment']
2022-01-04 14:33:19 +00:00
media_style_added = False
post_id = None
if post_json_object['object'].get('id'):
post_id = post_json_object['object']['id']
post_id = remove_id_ending(post_id).replace('/', '--')
2023-02-18 19:21:24 +00:00
2023-07-10 10:49:13 +00:00
# chat links
# https://codeberg.org/fediverse/fep/src/branch/main/fep/1970/fep-1970.md
2023-10-30 10:21:37 +00:00
attached_urls = []
for attach in attachment_dict:
2023-07-10 10:49:13 +00:00
if not attach.get('type') or \
not attach.get('name') or \
not attach.get('href') or \
not attach.get('rel'):
continue
if not isinstance(attach['type'], str) or \
not isinstance(attach['name'], str) or \
not isinstance(attach['href'], str) or \
not isinstance(attach['rel'], str):
continue
if attach['type'] != 'Link' or \
attach['name'] != 'Chat' or \
attach['rel'] != 'discussion' or \
'://' not in attach['href'] or \
'.' not in attach['href']:
continue
# get the domain for the chat link
chat_domain_str = ''
attach_url = remove_html(attach['href'])
2023-10-30 10:21:37 +00:00
if attach_url in attached_urls:
continue
attached_urls.append(attach_url)
chat_domain, _ = get_domain_from_actor(attach_url)
if chat_domain:
if local_network_host(chat_domain):
print('REJECT: local network chat link ' + attach['href'])
continue
chat_domain_str = ' (' + chat_domain + ')'
# avoid displaying very long domains
if len(chat_domain_str) > 50:
chat_domain_str = ''
2023-07-12 09:08:14 +00:00
chat_url = remove_html(attach['href'])
2023-07-10 10:53:20 +00:00
attachment_str += \
2023-07-12 09:08:14 +00:00
'<p><a href="' + chat_url + \
2023-07-10 10:49:13 +00:00
'" target="_blank" rel="nofollow noopener noreferrer">' + \
'💬 ' + translate['Chat'] + chat_domain_str + '</a></p>'
2023-07-10 10:49:13 +00:00
2023-02-18 19:21:24 +00:00
# obtain transcripts
transcripts = {}
for attach in attachment_dict:
2023-02-18 19:21:24 +00:00
if not attach.get('mediaType'):
continue
if attach['mediaType'] != 'text/vtt':
continue
name = None
if attach.get('name'):
name = attach['name']
2023-02-19 13:06:00 +00:00
if attach.get('nameMap'):
for name_lang, name_value in attach['nameMap'].items():
if not isinstance(name_value, str):
continue
if name_lang.startswith(system_language):
name = name_value
if not name and attach.get('hreflang'):
2023-02-18 19:21:24 +00:00
name = attach['hreflang']
url = None
if attach.get('url'):
2023-12-09 14:18:24 +00:00
url = get_url_from_post(attach['url'])
2023-02-18 19:21:24 +00:00
elif attach.get('href'):
url = attach['href']
if name and url:
2023-07-12 09:08:14 +00:00
transcripts[name] = remove_html(url)
2023-02-18 19:21:24 +00:00
for attach in attachment_dict:
2020-11-09 19:41:01 +00:00
if not (attach.get('mediaType') and attach.get('url')):
continue
media_license = ''
if attach.get('schema:license'):
if not dangerous_markup(attach['schema:license'], False, []):
if not is_filtered(base_dir, nickname, domain,
attach['schema:license'],
system_language):
if '://' not in attach['schema:license']:
if len(attach['schema:license']) < 60:
media_license = attach['schema:license']
else:
media_license = attach['schema:license']
elif attach.get('license'):
if not dangerous_markup(attach['license'], False, []):
if not is_filtered(base_dir, nickname, domain,
attach['license'],
system_language):
if '://' not in attach['license']:
if len(attach['license']) < 60:
media_license = attach['license']
else:
media_license = attach['license']
media_creator = ''
if attach.get('schema:creator'):
if len(attach['schema:creator']) < 120:
if not dangerous_markup(attach['schema:creator'], False, []):
if not is_filtered(base_dir, nickname, domain,
attach['schema:creator'],
system_language):
media_creator = attach['schema:creator']
elif attach.get('attribution'):
2023-05-17 20:52:59 +00:00
if isinstance(attach['attribution'], list):
if len(attach['attribution']) > 0:
attrib_str = attach['attribution'][0]
if not dangerous_markup(attrib_str, False, []):
2023-05-17 20:52:59 +00:00
if not is_filtered(base_dir, nickname, domain,
attrib_str, system_language):
media_creator = attrib_str
2020-11-09 19:41:01 +00:00
2022-01-04 14:33:19 +00:00
media_type = attach['mediaType']
image_description = ''
2020-11-09 19:41:01 +00:00
if attach.get('name'):
2022-01-04 14:33:19 +00:00
image_description = attach['name'].replace('"', "'")
image_description = remove_html(image_description)
2022-01-04 14:33:19 +00:00
if _is_image_mime_type(media_type):
2023-12-09 14:18:24 +00:00
url_str = get_url_from_post(attach['url'])
image_url = remove_html(url_str)
2023-10-30 10:37:20 +00:00
if image_url in attached_urls:
continue
attached_urls.append(image_url)
# display svg images if they have first been rendered harmless
svg_harmless = True
if 'svg' in media_type:
svg_harmless = False
if '://' + domain_full + '/' in image_url:
svg_harmless = True
else:
if post_id:
if '/' in image_url:
im_filename = image_url.split('/')[-1]
else:
im_filename = image_url
cached_svg_filename = \
base_dir + '/media/' + post_id + '_' + im_filename
if os.path.isfile(cached_svg_filename):
svg_harmless = True
2023-07-12 09:08:14 +00:00
if _is_attached_image(image_url) and svg_harmless:
2022-01-04 14:33:19 +00:00
if not attachment_str:
attachment_str += '<div class="media">\n'
media_style_added = True
if attachment_ctr > 0:
attachment_str += '<br>'
if box_name == 'tlmedia':
gallery_str += '<div class="gallery">\n'
2021-12-29 21:55:09 +00:00
if not is_muted:
2022-01-04 14:33:19 +00:00
gallery_str += ' <a href="' + image_url + '">\n'
if media_license and media_creator:
gallery_str += ' <figure>\n'
2022-01-04 14:33:19 +00:00
gallery_str += \
2022-03-28 08:47:53 +00:00
' <img loading="lazy" ' + \
'decoding="async" src="' + \
2022-01-04 14:33:19 +00:00
image_url + '" alt="" title="">\n'
gallery_str += ' </a>\n'
license_str = ''
if media_license and media_creator:
2023-07-12 09:08:14 +00:00
media_license = remove_html(media_license)
2024-01-27 17:04:21 +00:00
if resembles_url(media_license):
license_str += \
'<a href="' + media_license + \
'" target="_blank" ' + \
'rel="nofollow noopener noreferrer">©</a>'
else:
license_str += media_license
license_str += ' ' + media_creator
gallery_str += \
' ' + license_str + \
'</figcaption></figure>\n'
2021-12-25 22:09:19 +00:00
if post_json_object['object'].get('url'):
2023-12-09 14:18:24 +00:00
url_str = post_json_object['object']['url']
image_post_url = get_url_from_post(url_str)
2020-11-09 19:41:01 +00:00
else:
2022-01-04 14:33:19 +00:00
image_post_url = post_json_object['object']['id']
image_post_url = remove_html(image_post_url)
2022-01-04 14:33:19 +00:00
if image_description and not is_muted:
gallery_str += \
' <a href="' + image_post_url + \
2020-11-09 19:41:01 +00:00
'" class="gallerytext"><div ' + \
'class="gallerytext">' + \
2022-01-04 14:33:19 +00:00
image_description + '</div></a>\n'
2020-11-09 19:41:01 +00:00
else:
2022-01-04 14:33:19 +00:00
gallery_str += \
2020-11-09 19:41:01 +00:00
'<label class="transparent">---</label><br>'
2022-01-04 14:33:19 +00:00
gallery_str += ' <div class="mediaicons">\n'
# don't show the announce icon if there is no image
# description
if not image_description:
announce_str = ''
2022-01-04 14:33:19 +00:00
gallery_str += \
' ' + reply_str + announce_str + like_str + \
bookmark_str + delete_str + mute_str + '\n'
gallery_str += ' </div>\n'
gallery_str += ' <div class="mediaavatar">\n'
gallery_str += ' ' + avatar_link + '\n'
gallery_str += ' </div>\n'
gallery_str += '</div>\n'
# optionally hide the image
attributed_actor = None
minimize_images = False
2022-11-13 19:52:58 +00:00
if minimize_all_images:
minimize_images = True
if post_json_object['object'].get('attributedTo'):
attrib_field = post_json_object['object']['attributedTo']
attributed_actor = get_attributed_to(attrib_field)
if attributed_actor:
following_nickname = \
get_nickname_from_actor(attributed_actor)
following_domain, _ = \
get_domain_from_actor(attributed_actor)
if minimize_all_images:
minimize_images = True
else:
minimize_images = \
minimizing_attached_images(base_dir,
nickname, domain,
following_nickname,
following_domain)
# minimize any NSFW images
if not minimize_images and content:
if _is_nsfw(content):
minimize_images = True
if minimize_images:
show_img_str = 'SHOW MEDIA'
if translate:
show_img_str = translate['SHOW MEDIA']
attachment_str += \
'<details><summary class="cw" tabindex="10">' + \
show_img_str + '</summary>' + \
'<div id="' + post_id + '">\n'
2023-05-15 10:58:35 +00:00
attachment_str += \
2023-05-15 11:28:30 +00:00
'<a href="' + image_url + '" tabindex="10">'
2023-05-15 10:58:35 +00:00
if media_license and media_creator:
attachment_str += '<figure>'
2022-01-04 14:33:19 +00:00
attachment_str += \
2022-03-28 08:47:53 +00:00
'<img loading="lazy" decoding="async" ' + \
'src="' + image_url + \
2022-01-04 14:33:19 +00:00
'" alt="' + image_description + '" title="' + \
image_description + '" class="attachment"></a>\n'
if media_license and media_creator:
license_str = ''
attachment_str += '<figcaption>'
2023-07-12 09:08:14 +00:00
media_license = remove_html(media_license)
2024-01-27 17:04:21 +00:00
if resembles_url(media_license):
2023-01-23 14:50:09 +00:00
license_str += \
'<a href="' + media_license + \
'" target="_blank" ' + \
'rel="nofollow noopener noreferrer">©</a>'
else:
license_str += media_license
license_str += ' ' + media_creator
attachment_str += license_str + '</figcaption></figure>'
if minimize_images:
attachment_str += '</div></details>\n'
2022-01-04 14:33:19 +00:00
attachment_ctr += 1
elif _is_video_mime_type(media_type):
2023-07-12 09:08:14 +00:00
video_url = remove_html(attach['url'])
2023-10-30 10:37:20 +00:00
if video_url in attached_urls:
continue
attached_urls.append(video_url)
2023-07-12 09:08:14 +00:00
if _is_attached_video(video_url):
extension = video_url.split('.')[-1]
2022-01-04 14:33:19 +00:00
if attachment_ctr > 0:
attachment_str += '<br>'
if box_name == 'tlmedia':
gallery_str += '<div class="gallery">\n'
2023-10-30 10:21:37 +00:00
if post_json_object['object'].get('url'):
2023-12-09 14:18:24 +00:00
url_str = post_json_object['object']['url']
video_post_url = get_url_from_post(url_str)
2023-10-30 10:21:37 +00:00
else:
video_post_url = post_json_object['object']['id']
video_post_url = remove_html(video_post_url)
2021-12-29 21:55:09 +00:00
if not is_muted:
2022-05-25 13:33:59 +00:00
gallery_str += \
2023-07-12 09:08:14 +00:00
' <a href="' + video_url + \
2022-05-25 13:33:59 +00:00
'" tabindex="10">\n'
2022-01-04 14:33:19 +00:00
gallery_str += \
2021-03-07 11:55:06 +00:00
' <figure id="videoContainer" ' + \
'data-fullscreen="false">\n' + \
' <video id="video" controls ' + \
2022-06-10 16:32:38 +00:00
'preload="metadata" tabindex="10">\n'
2022-01-04 14:33:19 +00:00
gallery_str += \
2023-07-12 09:08:14 +00:00
' <source src="' + video_url + \
2022-01-04 14:33:19 +00:00
'" alt="' + image_description + \
'" title="' + image_description + \
2020-11-09 19:41:01 +00:00
'" class="attachment" type="video/' + \
2023-02-18 19:21:24 +00:00
extension + '">\n'
if transcripts:
for transcript_name, transcript_url in \
transcripts.items():
gallery_str += \
2023-10-29 21:01:53 +00:00
'<track src=”' + transcript_url + '" ' + \
'label=”' + transcript_name + '" ' + \
'srclang=”' + transcript_name + '" ' + \
'kind=”captions” >\n'
2023-02-19 14:41:15 +00:00
idx = 'Your browser does not support the video tag.'
gallery_str += translate[idx] + '\n'
2022-01-04 14:33:19 +00:00
gallery_str += ' </video>\n'
gallery_str += ' </figure>\n'
gallery_str += ' </a>\n'
if image_description and not is_muted:
gallery_str += \
' <a href="' + video_post_url + \
2022-05-25 13:33:59 +00:00
'" class="gallerytext" tabindex="10"><div ' + \
2020-11-09 19:41:01 +00:00
'class="gallerytext">' + \
2022-01-04 14:33:19 +00:00
image_description + '</div></a>\n'
2020-11-09 19:41:01 +00:00
else:
2022-01-04 14:33:19 +00:00
gallery_str += \
2020-11-09 19:41:01 +00:00
'<label class="transparent">---</label><br>'
2022-01-04 14:33:19 +00:00
gallery_str += ' <div class="mediaicons">\n'
gallery_str += \
' ' + reply_str + announce_str + like_str + \
bookmark_str + delete_str + mute_str + '\n'
gallery_str += ' </div>\n'
gallery_str += ' <div class="mediaavatar">\n'
gallery_str += ' ' + avatar_link + '\n'
gallery_str += ' </div>\n'
gallery_str += '</div>\n'
attachment_str += \
2021-03-07 12:01:33 +00:00
'<center><figure id="videoContainer" ' + \
'data-fullscreen="false">\n' + \
' <video id="video" controls ' + \
2022-06-10 16:32:38 +00:00
'preload="metadata" tabindex="10">\n'
2022-01-04 14:33:19 +00:00
attachment_str += \
2023-07-12 09:08:14 +00:00
' <source src="' + video_url + '" alt="' + \
2022-01-04 14:33:19 +00:00
image_description + '" title="' + image_description + \
2020-11-09 19:41:01 +00:00
'" class="attachment" type="video/' + \
2023-02-18 19:21:24 +00:00
extension + '">\n'
if transcripts:
for transcript_name, transcript_url in \
transcripts.items():
attachment_str += \
2023-10-29 21:01:53 +00:00
' <track src=”' + transcript_url + '" ' + \
'label=”' + transcript_name + '" ' + \
'srclang=”' + transcript_name + '" ' + \
'kind=”captions” >\n'
2022-01-04 14:33:19 +00:00
attachment_str += \
2020-11-09 19:41:01 +00:00
translate['Your browser does not support the video tag.']
2023-02-18 19:21:24 +00:00
attachment_str += '\n </video></figure></center>'
2022-01-04 14:33:19 +00:00
attachment_ctr += 1
elif _is_audio_mime_type(media_type):
2020-11-09 19:41:01 +00:00
extension = '.mp3'
2023-12-09 14:18:24 +00:00
url_str = get_url_from_post(attach['url'])
audio_url = remove_html(url_str)
2023-10-30 10:37:20 +00:00
if audio_url in attached_urls:
continue
attached_urls.append(audio_url)
2023-07-12 09:08:14 +00:00
if audio_url.endswith('.ogg'):
2020-11-09 19:41:01 +00:00
extension = '.ogg'
2023-07-12 09:08:14 +00:00
elif audio_url.endswith('.wav'):
2022-10-31 11:05:11 +00:00
extension = '.wav'
2023-07-12 09:08:14 +00:00
elif audio_url.endswith('.opus'):
2022-04-18 13:21:45 +00:00
extension = '.opus'
2023-07-12 09:08:14 +00:00
elif audio_url.endswith('.spx'):
2022-10-20 19:37:59 +00:00
extension = '.spx'
2023-07-12 09:08:14 +00:00
elif audio_url.endswith('.flac'):
2022-04-18 13:44:08 +00:00
extension = '.flac'
2023-07-12 09:08:14 +00:00
if audio_url.endswith(extension):
2022-01-04 14:33:19 +00:00
if attachment_ctr > 0:
attachment_str += '<br>'
if box_name == 'tlmedia':
gallery_str += '<div class="gallery">\n'
2021-12-29 21:55:09 +00:00
if not is_muted:
2022-05-25 13:33:59 +00:00
gallery_str += \
2023-07-12 09:08:14 +00:00
' <a href="' + audio_url + \
2022-05-25 13:33:59 +00:00
'" tabindex="10">\n'
2022-06-10 16:32:38 +00:00
gallery_str += ' <audio controls tabindex="10">\n'
2022-01-04 14:33:19 +00:00
gallery_str += \
2023-07-12 09:08:14 +00:00
' <source src="' + audio_url + \
2022-01-04 14:33:19 +00:00
'" alt="' + image_description + \
'" title="' + image_description + \
2020-11-09 19:41:01 +00:00
'" class="attachment" type="audio/' + \
extension.replace('.', '') + '">'
idx = 'Your browser does not support the audio tag.'
2022-01-04 14:33:19 +00:00
gallery_str += translate[idx]
gallery_str += ' </audio>\n'
gallery_str += ' </a>\n'
2021-12-25 22:09:19 +00:00
if post_json_object['object'].get('url'):
2023-12-09 14:18:24 +00:00
url_str = post_json_object['object']['url']
audio_post_url = get_url_from_post(url_str)
2020-11-09 19:41:01 +00:00
else:
2022-01-04 14:33:19 +00:00
audio_post_url = post_json_object['object']['id']
audio_post_url = remove_html(audio_post_url)
2022-01-04 14:33:19 +00:00
if image_description and not is_muted:
gallery_str += \
' <a href="' + audio_post_url + \
2020-11-09 19:41:01 +00:00
'" class="gallerytext"><div ' + \
'class="gallerytext">' + \
2022-01-04 14:33:19 +00:00
image_description + '</div></a>\n'
2020-11-09 19:41:01 +00:00
else:
2022-01-04 14:33:19 +00:00
gallery_str += \
2020-11-09 19:41:01 +00:00
'<label class="transparent">---</label><br>'
2022-01-04 14:33:19 +00:00
gallery_str += ' <div class="mediaicons">\n'
gallery_str += \
' ' + reply_str + announce_str + \
like_str + bookmark_str + \
delete_str + mute_str + '\n'
gallery_str += ' </div>\n'
gallery_str += ' <div class="mediaavatar">\n'
gallery_str += ' ' + avatar_link + '\n'
gallery_str += ' </div>\n'
gallery_str += '</div>\n'
2022-06-10 16:32:38 +00:00
attachment_str += '<center>\n<audio controls tabindex="10">\n'
2022-01-04 14:33:19 +00:00
attachment_str += \
2023-07-12 09:08:14 +00:00
'<source src="' + audio_url + '" alt="' + \
2022-01-04 14:33:19 +00:00
image_description + '" title="' + image_description + \
2020-11-09 19:41:01 +00:00
'" class="attachment" type="audio/' + \
extension.replace('.', '') + '">'
2022-01-04 14:33:19 +00:00
attachment_str += \
2020-11-09 19:41:01 +00:00
translate['Your browser does not support the audio tag.']
2022-01-04 14:33:19 +00:00
attachment_str += '</audio>\n</center>\n'
attachment_ctr += 1
if media_style_added:
2022-07-11 19:00:06 +00:00
attachment_str += '</div><br>'
2023-07-10 10:53:20 +00:00
return attachment_str, gallery_str
2020-11-09 19:41:01 +00:00
2021-12-29 21:55:09 +00:00
def html_post_separator(base_dir: str, column: str) -> str:
2020-11-09 19:41:01 +00:00
"""Returns the html for a timeline post separator image
"""
2021-12-26 14:08:58 +00:00
theme = get_config_param(base_dir, 'theme')
2023-10-12 13:07:23 +00:00
if not theme:
theme = 'default'
2020-11-09 19:41:01 +00:00
filename = 'separator.png'
2022-01-04 14:33:19 +00:00
separator_class = "postSeparatorImage"
2020-11-09 19:41:01 +00:00
if column:
2022-01-04 14:33:19 +00:00
separator_class = "postSeparatorImage" + column.title()
2020-11-09 19:41:01 +00:00
filename = 'separator_' + column + '.png'
2022-01-04 14:33:19 +00:00
separator_image_filename = \
2021-12-25 16:17:53 +00:00
base_dir + '/theme/' + theme + '/icons/' + filename
2022-01-04 14:33:19 +00:00
separator_str = ''
if os.path.isfile(separator_image_filename):
separator_str = \
'<div class="' + separator_class + '"><center>' + \
2021-02-01 18:38:08 +00:00
'<img src="/icons/' + filename + '" ' + \
'alt="" /></center></div>\n'
2022-01-04 14:33:19 +00:00
return separator_str
2020-11-09 22:44:03 +00:00
2021-12-29 21:55:09 +00:00
def html_highlight_label(label: str, highlight: bool) -> str:
2020-11-17 20:40:36 +00:00
"""If the given text should be highlighted then return
the appropriate markup.
This is so that in shell browsers, like lynx, it's possible
to see if the replies or DM button are highlighted.
"""
if not highlight:
return label
return '*' + str(label) + '*'
2022-06-01 17:45:59 +00:00
def get_avatar_image_url(session, base_dir: str, http_prefix: str,
2022-01-04 14:33:19 +00:00
post_actor: str, person_cache: {},
avatar_url: str, allow_downloads: bool,
2021-12-29 21:55:09 +00:00
signing_priv_key_pem: str) -> str:
"""Returns the avatar image url
"""
# get the avatar image url for the post actor
2022-01-04 14:33:19 +00:00
if not avatar_url:
avatar_url = \
2022-06-12 12:30:14 +00:00
get_person_avatar_url(base_dir, post_actor, person_cache)
2022-01-04 14:33:19 +00:00
avatar_url = \
2021-12-29 21:55:09 +00:00
update_avatar_image_cache(signing_priv_key_pem,
session, base_dir, http_prefix,
2022-01-04 14:33:19 +00:00
post_actor, avatar_url, person_cache,
allow_downloads)
else:
2021-12-29 21:55:09 +00:00
update_avatar_image_cache(signing_priv_key_pem,
session, base_dir, http_prefix,
2022-01-04 14:33:19 +00:00
post_actor, avatar_url, person_cache,
allow_downloads)
2022-01-04 14:33:19 +00:00
if not avatar_url:
avatar_url = post_actor + '/avatar.png'
2022-01-04 14:33:19 +00:00
return avatar_url
2021-02-05 17:05:53 +00:00
2022-01-04 14:33:19 +00:00
def html_hide_from_screen_reader(html_str: str) -> str:
2021-02-06 10:35:47 +00:00
"""Returns html which is hidden from screen readers
"""
2022-01-04 14:33:19 +00:00
return '<span aria-hidden="true">' + html_str + '</span>'
2021-02-06 10:35:47 +00:00
2021-12-31 21:18:12 +00:00
def html_keyboard_navigation(banner: str, links: {}, access_keys: {},
2024-02-19 18:31:04 +00:00
sub_heading: str,
users_path: str, translate: {},
follow_approvals: bool) -> str:
2021-02-05 17:05:53 +00:00
"""Given a set of links return the html for keyboard navigation
"""
2022-01-04 14:33:19 +00:00
html_str = '<div class="transparent"><ul>\n'
2021-02-05 19:15:52 +00:00
if banner:
2022-01-04 14:33:19 +00:00
html_str += '<pre aria-label="">\n' + banner + '\n<br><br></pre>\n'
2021-02-05 19:15:52 +00:00
2022-01-04 14:33:19 +00:00
if sub_heading:
html_str += '<strong><label class="transparent">' + \
sub_heading + '</label></strong><br>\n'
2021-02-12 15:28:11 +00:00
# show new follower approvals
2022-01-04 14:33:19 +00:00
if users_path and translate and follow_approvals:
html_str += '<strong><label class="transparent">' + \
'<a href="' + users_path + '/followers#timeline" ' + \
'tabindex="-1">' + \
translate['Approve follow requests'] + '</a>' + \
'</label></strong><br><br>\n'
# show the list of links
2021-02-05 17:05:53 +00:00
for title, url in links.items():
2022-01-04 14:33:19 +00:00
access_key_str = ''
2021-12-31 21:18:12 +00:00
if access_keys.get(title):
2022-01-04 14:33:19 +00:00
access_key_str = 'accesskey="' + access_keys[title] + '"'
2021-04-22 11:51:19 +00:00
2022-01-04 14:33:19 +00:00
html_str += '<li><label class="transparent">' + \
'<a href="' + str(url) + '" ' + access_key_str + \
' tabindex="-1">' + \
str(title) + '</a></label></li>\n'
2022-01-04 14:33:19 +00:00
html_str += '</ul></div>\n'
return html_str
2021-07-22 16:58:59 +00:00
2021-12-29 21:55:09 +00:00
def begin_edit_section(label: str) -> str:
2021-07-22 16:58:59 +00:00
"""returns the html for begining a dropdown section on edit profile screen
"""
return \
' <details><summary class="cw">' + label + '</summary>\n' + \
'<div class="container">'
2021-12-29 21:55:09 +00:00
def end_edit_section() -> str:
2021-07-22 16:58:59 +00:00
"""returns the html for ending a dropdown section on edit profile screen
"""
return ' </div></details>\n'
2021-12-29 21:55:09 +00:00
def edit_text_field(label: str, name: str, value: str = "",
placeholder: str = "", required: bool = False) -> str:
2021-07-22 16:58:59 +00:00
"""Returns html for editing a text field
"""
if value is None:
value = ''
2022-01-04 14:33:19 +00:00
placeholder_str = ''
2021-07-22 16:58:59 +00:00
if placeholder:
2022-01-04 14:33:19 +00:00
placeholder_str = ' placeholder="' + placeholder + '"'
required_str = ''
2021-07-27 18:31:50 +00:00
if required:
2022-01-04 14:33:19 +00:00
required_str = ' required'
text_field_str = ''
if label:
2022-01-04 14:33:19 +00:00
text_field_str = \
'<label class="labels">' + label + '</label><br>\n'
2022-01-04 14:33:19 +00:00
text_field_str += \
2021-07-22 16:58:59 +00:00
' <input type="text" name="' + name + '" value="' + \
2022-01-04 14:33:19 +00:00
value + '"' + placeholder_str + required_str + '>\n'
return text_field_str
2021-07-22 16:58:59 +00:00
2021-12-29 21:55:09 +00:00
def edit_number_field(label: str, name: str, value: int,
2022-01-04 14:33:19 +00:00
min_value: int, max_value: int,
2021-12-29 21:55:09 +00:00
placeholder: int) -> str:
2021-07-24 11:47:51 +00:00
"""Returns html for editing an integer number field
"""
if value is None:
value = ''
2022-01-04 14:33:19 +00:00
placeholder_str = ''
2021-07-24 11:47:51 +00:00
if placeholder:
2022-01-04 14:33:19 +00:00
placeholder_str = ' placeholder="' + str(placeholder) + '"'
2021-07-24 11:47:51 +00:00
return \
'<label class="labels">' + label + '</label><br>\n' + \
' <input type="number" name="' + name + '" value="' + \
2022-01-04 14:33:19 +00:00
str(value) + '"' + placeholder_str + ' ' + \
'min="' + str(min_value) + '" max="' + str(max_value) + '" step="1">\n'
2021-07-24 11:47:51 +00:00
2021-12-29 21:55:09 +00:00
def edit_currency_field(label: str, name: str, value: str,
placeholder: str, required: bool) -> str:
2021-07-24 22:08:11 +00:00
"""Returns html for editing a currency field
"""
if value is None:
value = '0.00'
2022-01-04 14:33:19 +00:00
placeholder_str = ''
2021-07-24 22:08:11 +00:00
if placeholder:
if placeholder.isdigit():
2022-01-04 14:33:19 +00:00
placeholder_str = ' placeholder="' + str(placeholder) + '"'
required_str = ''
2021-07-27 18:56:51 +00:00
if required:
2022-01-04 14:33:19 +00:00
required_str = ' required'
2021-07-24 22:08:11 +00:00
return \
'<label class="labels">' + label + '</label><br>\n' + \
' <input type="text" name="' + name + '" value="' + \
2022-01-04 14:33:19 +00:00
str(value) + '"' + placeholder_str + ' ' + \
2021-07-27 18:56:51 +00:00
' pattern="^\\d{1,3}(,\\d{3})*(\\.\\d+)?" data-type="currency"' + \
2022-01-04 14:33:19 +00:00
required_str + '>\n'
2021-07-24 22:08:11 +00:00
2021-12-29 21:55:09 +00:00
def edit_check_box(label: str, name: str, checked: bool) -> str:
2021-07-22 16:58:59 +00:00
"""Returns html for editing a checkbox field
"""
2022-01-04 14:33:19 +00:00
checked_str = ''
2021-07-22 16:58:59 +00:00
if checked:
2022-01-04 14:33:19 +00:00
checked_str = ' checked'
2021-07-22 16:58:59 +00:00
return \
' <input type="checkbox" class="profilecheckbox" ' + \
2022-01-04 14:33:19 +00:00
'name="' + name + '"' + checked_str + '> ' + label + '<br>\n'
2021-07-22 16:58:59 +00:00
2022-09-02 15:57:06 +00:00
def edit_text_area(label: str, subtitle: str, name: str, value: str,
2021-12-29 21:55:09 +00:00
height: int, placeholder: str, spellcheck: bool) -> str:
2021-07-22 16:58:59 +00:00
"""Returns html for editing a textarea field
"""
if value is None:
value = ''
2021-07-22 18:35:45 +00:00
text = ''
if label:
text = '<label class="labels">' + label + '</label><br>\n'
2022-09-02 16:00:38 +00:00
if subtitle:
2022-09-02 16:04:03 +00:00
text += subtitle + '<br>\n'
2021-07-22 18:35:45 +00:00
text += \
2021-07-22 16:58:59 +00:00
' <textarea id="message" placeholder=' + \
2021-07-22 18:50:31 +00:00
'"' + placeholder + '" '
text += 'name="' + name + '" '
2021-07-22 18:52:47 +00:00
text += 'style="height:' + str(height) + 'px" '
2021-07-22 18:50:31 +00:00
text += 'spellcheck="' + str(spellcheck).lower() + '">'
text += value + '</textarea>\n'
2021-07-22 18:35:45 +00:00
return text
2022-01-04 14:33:19 +00:00
def html_search_result_share(base_dir: str, shared_item: {}, translate: {},
2021-12-29 21:55:09 +00:00
http_prefix: str, domain_full: str,
2022-01-04 14:33:19 +00:00
contact_nickname: str, item_id: str,
actor: str, shares_file_type: str,
category: str,
publicly_visible: bool) -> str:
"""Returns the html for an individual shared item
"""
2022-01-04 14:33:19 +00:00
shared_items_form = '<div class="container">\n'
shared_items_form += \
'<p class="share-title">' + shared_item['displayName'] + '</p>\n'
if shared_item.get('imageUrl'):
shared_items_form += \
'<a href="' + shared_item['imageUrl'] + '">\n'
shared_items_form += \
2022-03-28 08:47:53 +00:00
'<img loading="lazy" decoding="async" ' + \
'src="' + shared_item['imageUrl'] + \
'" alt="Item image"></a>\n'
2022-01-04 14:33:19 +00:00
shared_items_form += '<p>' + shared_item['summary'] + '</p>\n<p>'
if shared_item.get('itemQty'):
if shared_item['itemQty'] > 1:
shared_items_form += \
'<b>' + translate['Quantity'] + \
2022-01-04 14:33:19 +00:00
':</b> ' + str(shared_item['itemQty']) + '<br>'
shared_items_form += \
'<b>' + translate['Type'] + ':</b> ' + shared_item['itemType'] + '<br>'
shared_items_form += \
'<b>' + translate['Category'] + ':</b> ' + \
2022-01-04 14:33:19 +00:00
shared_item['category'] + '<br>'
if shared_item.get('location'):
shared_items_form += \
'<b>' + translate['Location'] + ':</b> ' + \
2022-01-04 14:33:19 +00:00
shared_item['location'] + '<br>'
contact_title_str = translate['Contact']
if shared_item.get('itemPrice') and \
shared_item.get('itemCurrency'):
if is_float(shared_item['itemPrice']):
if float(shared_item['itemPrice']) > 0:
shared_items_form += \
' <b>' + translate['Price'] + \
2022-01-04 14:33:19 +00:00
':</b> ' + shared_item['itemPrice'] + \
' ' + shared_item['itemCurrency']
contact_title_str = translate['Buy']
shared_items_form += '</p>\n'
contact_actor = \
local_actor_url(http_prefix, contact_nickname, domain_full)
button_style_str = 'button'
2021-09-19 15:54:51 +00:00
if category == 'accommodation':
2022-01-04 14:33:19 +00:00
contact_title_str = translate['Request to stay']
button_style_str = 'contactbutton'
2021-09-19 15:54:51 +00:00
if not publicly_visible:
shared_items_form += \
'<p>' + \
'<a href="' + actor + '?replydm=sharedesc:' + \
shared_item['displayName'] + '?mention=' + contact_actor + \
'?category=' + category + '">' + \
'<button class="' + button_style_str + '">' + contact_title_str + \
'</button></a>\n' + \
'<a href="' + contact_actor + '"><button class="button">' + \
translate['Profile'] + '</button></a>\n'
else:
shared_items_form += \
'<a href="' + contact_actor + '"><button class="button">' + \
translate['Contact'] + '</button></a>\n'
# should the remove button be shown?
2022-01-04 14:33:19 +00:00
show_remove_button = False
2021-12-27 22:19:18 +00:00
nickname = get_nickname_from_actor(actor)
if not nickname:
return ''
2022-01-04 14:33:19 +00:00
if actor.endswith('/users/' + contact_nickname):
show_remove_button = True
2021-12-28 19:33:29 +00:00
elif is_moderator(base_dir, nickname):
2022-01-04 14:33:19 +00:00
show_remove_button = True
else:
2021-12-31 21:18:12 +00:00
admin_nickname = get_config_param(base_dir, 'admin')
if admin_nickname:
if actor.endswith('/users/' + admin_nickname):
2022-01-04 14:33:19 +00:00
show_remove_button = True
if show_remove_button and not publicly_visible:
2022-01-04 14:33:19 +00:00
if shares_file_type == 'shares':
shared_items_form += \
2021-08-09 22:07:34 +00:00
' <a href="' + actor + '?rmshare=' + \
2022-01-04 14:33:19 +00:00
item_id + '"><button class="button">' + \
2021-08-09 22:07:34 +00:00
translate['Remove'] + '</button></a>\n'
else:
2022-01-04 14:33:19 +00:00
shared_items_form += \
2021-08-09 22:07:34 +00:00
' <a href="' + actor + '?rmwanted=' + \
2022-01-04 14:33:19 +00:00
item_id + '"><button class="button">' + \
2021-08-09 22:07:34 +00:00
translate['Remove'] + '</button></a>\n'
2022-01-04 14:33:19 +00:00
shared_items_form += '</p></div>\n'
return shared_items_form
2021-12-29 21:55:09 +00:00
def html_show_share(base_dir: str, domain: str, nickname: str,
http_prefix: str, domain_full: str,
2022-01-04 14:33:19 +00:00
item_id: str, translate: {},
2021-12-29 21:55:09 +00:00
shared_items_federated_domains: [],
2021-12-31 23:50:29 +00:00
default_timeline: str, theme: str,
shares_file_type: str, category: str,
publicly_visible: bool) -> str:
"""Shows an individual shared item after selecting it from the left column
"""
2022-01-04 14:33:19 +00:00
shares_json = None
2022-01-04 14:33:19 +00:00
share_url = item_id.replace('___', '://').replace('--', '/')
contact_nickname = get_nickname_from_actor(share_url)
if not contact_nickname:
return None
2022-01-04 14:33:19 +00:00
if '://' + domain_full + '/' in share_url:
# shared item on this instance
2022-01-04 14:33:19 +00:00
shares_filename = \
acct_dir(base_dir, contact_nickname, domain) + '/' + \
shares_file_type + '.json'
if not os.path.isfile(shares_filename):
return None
2022-01-04 14:33:19 +00:00
shares_json = load_json(shares_filename)
else:
# federated shared item
2022-01-04 14:33:19 +00:00
if shares_file_type == 'shares':
catalogs_dir = base_dir + '/cache/catalogs'
else:
2022-01-04 14:33:19 +00:00
catalogs_dir = base_dir + '/cache/wantedItems'
if not os.path.isdir(catalogs_dir):
return None
2022-01-04 14:33:19 +00:00
for _, _, files in os.walk(catalogs_dir):
for fname in files:
if '#' in fname:
continue
2022-01-04 14:33:19 +00:00
if not fname.endswith('.' + shares_file_type + '.json'):
continue
2022-01-04 14:33:19 +00:00
federated_domain = fname.split('.')[0]
if federated_domain not in shared_items_federated_domains:
continue
2022-01-04 14:33:19 +00:00
shares_filename = catalogs_dir + '/' + fname
shares_json = load_json(shares_filename)
if not shares_json:
continue
2022-01-04 14:33:19 +00:00
if shares_json.get(item_id):
break
break
2022-01-04 14:33:19 +00:00
if not shares_json:
return None
2022-01-04 14:33:19 +00:00
if not shares_json.get(item_id):
return None
2022-01-04 14:33:19 +00:00
shared_item = shares_json[item_id]
2021-12-26 10:19:59 +00:00
actor = local_actor_url(http_prefix, nickname, domain_full)
# filename of the banner shown at the top
2022-01-04 14:33:19 +00:00
banner_file, _ = \
2021-12-29 21:55:09 +00:00
get_banner_file(base_dir, nickname, domain, theme)
2022-01-04 14:33:19 +00:00
share_str = \
'<header>\n' + \
'<a href="/users/' + nickname + '/' + \
2021-12-31 23:50:29 +00:00
default_timeline + '" title="" alt="">\n'
2022-03-28 08:47:53 +00:00
share_str += '<img loading="lazy" decoding="async" ' + \
'class="timeline-banner" alt="" ' + \
2021-12-31 21:18:12 +00:00
'src="/users/' + nickname + '/' + banner_file + '" /></a>\n' + \
'</header><br>\n'
2022-01-04 14:33:19 +00:00
share_str += \
html_search_result_share(base_dir, shared_item, translate, http_prefix,
domain_full, contact_nickname, item_id,
actor, shares_file_type, category,
publicly_visible)
2021-12-31 21:18:12 +00:00
css_filename = base_dir + '/epicyon-profile.css'
2021-12-25 16:17:53 +00:00
if os.path.isfile(base_dir + '/epicyon.css'):
2021-12-31 21:18:12 +00:00
css_filename = base_dir + '/epicyon.css'
2022-01-04 14:33:19 +00:00
instance_title = \
2021-12-26 14:08:58 +00:00
get_config_param(base_dir, 'instanceTitle')
2021-12-31 21:18:12 +00:00
return html_header_with_external_style(css_filename,
2022-01-04 14:33:19 +00:00
instance_title, None) + \
share_str + html_footer()
2021-10-30 11:08:57 +00:00
2021-12-29 21:55:09 +00:00
def set_custom_background(base_dir: str, background: str,
2022-01-04 14:33:19 +00:00
new_background: str) -> str:
2021-10-30 11:08:57 +00:00
"""Sets a custom background
Returns the extension, if found
"""
2021-10-30 11:51:41 +00:00
ext = 'jpg'
2021-12-25 16:17:53 +00:00
if os.path.isfile(base_dir + '/img/' + background + '.' + ext):
2022-01-04 14:33:19 +00:00
if not new_background:
new_background = background
2021-12-25 16:17:53 +00:00
if not os.path.isfile(base_dir + '/accounts/' +
2022-01-04 14:33:19 +00:00
new_background + '.' + ext):
2021-12-25 16:17:53 +00:00
copyfile(base_dir + '/img/' + background + '.' + ext,
2022-01-04 14:33:19 +00:00
base_dir + '/accounts/' + new_background + '.' + ext)
2021-10-30 11:51:41 +00:00
return ext
2021-10-30 11:08:57 +00:00
return None
def html_common_emoji(base_dir: str, no_of_emoji: int) -> str:
"""Shows common emoji
"""
emojis_filename = base_dir + '/emoji/emoji.json'
if not os.path.isfile(emojis_filename):
emojis_filename = base_dir + '/emoji/default_emoji.json'
2022-04-19 14:27:59 +00:00
emojis_json = load_json(emojis_filename)
common_emoji_filename = base_dir + '/accounts/common_emoji.txt'
if not os.path.isfile(common_emoji_filename):
return ''
common_emoji = None
try:
2022-06-09 14:46:30 +00:00
with open(common_emoji_filename, 'r', encoding='utf-8') as fp_emoji:
common_emoji = fp_emoji.readlines()
except OSError:
print('EX: html_common_emoji unable to load file')
return ''
if not common_emoji:
return ''
line_ctr = 0
ctr = 0
html_str = ''
while ctr < no_of_emoji and line_ctr < len(common_emoji):
2022-06-21 11:58:50 +00:00
emoji_name1 = common_emoji[line_ctr].split(' ')[1]
emoji_name = remove_eol(emoji_name1)
emoji_icon_name = emoji_name
emoji_filename = base_dir + '/emoji/' + emoji_name + '.png'
if not os.path.isfile(emoji_filename):
2022-04-19 14:27:59 +00:00
emoji_filename = base_dir + '/customemoji/' + emoji_name + '.png'
if not os.path.isfile(emoji_filename):
# load the emojis index
if not emojis_json:
emojis_json = load_json(emojis_filename)
# lookup the name within the index to get the hex code
if emojis_json:
for emoji_tag, emoji_code in emojis_json.items():
if emoji_tag == emoji_name:
# get the filename based on the hex code
emoji_filename = \
2022-04-19 14:30:50 +00:00
base_dir + '/emoji/' + emoji_code + '.png'
emoji_icon_name = emoji_code
2022-04-19 14:27:59 +00:00
break
if os.path.isfile(emoji_filename):
# NOTE: deliberately no alt text, so that without graphics only
# the emoji name shows
html_str += \
2022-04-19 11:48:18 +00:00
'<label class="hashtagswarm">' + \
2022-04-19 12:05:42 +00:00
'<img id="commonemojilabel" ' + \
2022-04-19 11:48:18 +00:00
'loading="lazy" decoding="async" ' + \
'src="/emoji/' + emoji_icon_name + '.png" ' + \
'alt="" title="">' + \
':' + emoji_name + ':</label>\n'
ctr += 1
line_ctr += 1
return html_str
def text_mode_browser(ua_str: str) -> bool:
"""Does the user agent indicate a text mode browser?
"""
2023-03-23 18:56:36 +00:00
if ua_str:
text_mode_agents = ('Lynx/', 'w3m/', 'Links (', 'Emacs/', 'ELinks')
for agent in text_mode_agents:
if agent in ua_str:
return True
return False
2022-12-18 09:44:11 +00:00
def get_default_path(media_instance: bool, blogs_instance: bool,
nickname: str) -> str:
"""Returns the default timeline
"""
if blogs_instance:
path = '/users/' + nickname + '/tlblogs'
elif media_instance:
path = '/users/' + nickname + '/tlmedia'
else:
path = '/users/' + nickname + '/inbox'
return path
2022-12-31 21:32:49 +00:00
def html_following_data_list(base_dir: str, nickname: str,
domain: str, domain_full: str,
following_type: str,
use_petnames: bool) -> str:
2022-12-31 21:32:49 +00:00
"""Returns a datalist of handles being followed
2022-12-31 21:33:10 +00:00
followingHandles, followersHandles
2022-12-31 21:32:49 +00:00
"""
list_str = '<datalist id="' + following_type + 'Handles">\n'
following_filename = \
acct_dir(base_dir, nickname, domain) + '/' + following_type + '.txt'
msg = None
if os.path.isfile(following_filename):
with open(following_filename, 'r',
encoding='utf-8') as following_file:
msg = following_file.read()
# add your own handle, so that you can send DMs
# to yourself as reminders
msg += nickname + '@' + domain_full + '\n'
if msg:
# include petnames
petnames_filename = \
acct_dir(base_dir, nickname, domain) + '/petnames.txt'
if use_petnames and os.path.isfile(petnames_filename):
2022-12-31 21:32:49 +00:00
following_list = []
with open(petnames_filename, 'r',
encoding='utf-8') as petnames_file:
pet_str = petnames_file.read()
# extract each petname and append it
petnames_list = pet_str.split('\n')
for pet in petnames_list:
following_list.append(pet.split(' ')[0])
# add the following.txt entries
following_list += msg.split('\n')
else:
# no petnames list exists - just use following.txt
following_list = msg.split('\n')
following_list.sort()
if following_list:
for following_address in following_list:
2022-12-31 23:12:03 +00:00
if not following_address:
continue
if '@' not in following_address and \
'://' not in following_address:
continue
list_str += '<option>@' + following_address + '</option>\n'
2022-12-31 21:32:49 +00:00
list_str += '</datalist>\n'
return list_str
def html_following_dropdown(base_dir: str, nickname: str,
domain: str, domain_full: str,
following_type: str,
use_petnames: bool) -> str:
"""Returns a select list of handles being followed or of followers
"""
list_str = '<select name="searchtext">\n'
following_filename = \
acct_dir(base_dir, nickname, domain) + '/' + following_type + '.txt'
msg = None
if os.path.isfile(following_filename):
with open(following_filename, 'r',
encoding='utf-8') as following_file:
msg = following_file.read()
# add your own handle, so that you can send DMs
# to yourself as reminders
msg += nickname + '@' + domain_full + '\n'
if msg:
# include petnames
petnames_filename = \
acct_dir(base_dir, nickname, domain) + '/petnames.txt'
if use_petnames and os.path.isfile(petnames_filename):
following_list = []
with open(petnames_filename, 'r',
encoding='utf-8') as petnames_file:
pet_str = petnames_file.read()
# extract each petname and append it
petnames_list = pet_str.split('\n')
for pet in petnames_list:
following_list.append(pet.split(' ')[0])
# add the following.txt entries
following_list += msg.split('\n')
else:
# no petnames list exists - just use following.txt
following_list = msg.split('\n')
2023-01-01 10:32:08 +00:00
list_str += '<option value="" selected></option>\n'
if following_list:
2023-01-01 10:49:40 +00:00
domain_sorted_list = []
for following_address in following_list:
if '@' not in following_address and \
'://' not in following_address:
continue
2023-01-01 10:49:40 +00:00
foll_nick = get_nickname_from_actor(following_address)
foll_domain, _ = get_domain_from_actor(following_address)
if not foll_domain or not foll_nick:
continue
domain_sorted_list.append(foll_domain + ' ' +
foll_nick + '@' + foll_domain)
domain_sorted_list.sort()
2023-01-01 10:53:54 +00:00
prev_foll_domain = ''
2023-01-01 10:49:40 +00:00
for following_line in domain_sorted_list:
following_address = following_line.split(' ')[1]
2023-01-01 10:53:54 +00:00
foll_domain, _ = get_domain_from_actor(following_address)
if prev_foll_domain and prev_foll_domain != foll_domain:
2023-01-01 11:02:02 +00:00
list_str += '<option value="" disabled></option>\n'
2023-01-01 10:53:54 +00:00
prev_foll_domain = foll_domain
list_str += '<option value="' + following_address + '">' + \
following_address + '</option>\n'
list_str += '</select>\n'
return list_str
2023-01-13 15:04:48 +00:00
def get_buy_links(post_json_object: str, translate: {}, buy_sites: {}) -> {}:
"""Returns any links to buy something from an external site
"""
2023-01-13 21:43:14 +00:00
if not post_json_object['object'].get('attachment'):
2023-01-13 15:04:48 +00:00
return {}
2023-01-13 21:43:14 +00:00
if not isinstance(post_json_object['object']['attachment'], list):
2023-01-13 15:04:48 +00:00
return {}
links = {}
buy_strings = []
2023-01-13 19:19:57 +00:00
for buy_str in ('Buy', 'Purchase', 'Subscribe'):
if translate.get(buy_str):
buy_str = translate[buy_str]
buy_strings += buy_str.lower()
2023-04-23 09:11:29 +00:00
buy_strings += ('Paypal', 'Stripe', 'Cashapp', 'Venmo')
2023-01-13 21:43:14 +00:00
for item in post_json_object['object']['attachment']:
2023-01-13 15:04:48 +00:00
if not isinstance(item, dict):
continue
if not item.get('name'):
continue
if not isinstance(item['name'], str):
continue
if not item.get('type'):
continue
if not item.get('href'):
continue
if not isinstance(item['type'], str):
continue
if not isinstance(item['href'], str):
continue
if item['type'] != 'Link':
continue
if not item.get('mediaType'):
continue
if not isinstance(item['mediaType'], str):
continue
if 'html' not in item['mediaType']:
continue
item_name = item['name']
2023-01-13 15:30:15 +00:00
# The name should not be excessively long
if len(item_name) > 32:
continue
2023-01-13 15:04:48 +00:00
# there should be no html in the name
if remove_html(item_name) != item_name:
continue
# there should be no html in the link
if '<' in item['href'] or \
2023-01-13 15:30:15 +00:00
'://' not in item['href'] or \
' ' in item['href']:
2023-01-13 15:04:48 +00:00
continue
if item.get('rel'):
2023-04-23 09:18:20 +00:00
if isinstance(item['rel'], str):
2023-04-23 09:19:42 +00:00
if item['rel'] in ('payment', 'pay', 'donate', 'donation',
'buy', 'purchase', 'support'):
2023-07-12 08:53:32 +00:00
links[item_name] = remove_html(item['href'])
2023-04-23 09:18:20 +00:00
continue
2023-01-13 15:30:15 +00:00
if buy_sites:
# limited to an allowlist of buying sites
for site, buy_domain in buy_sites.items():
if buy_domain in item['href']:
2023-07-12 08:53:32 +00:00
links[site.title()] = remove_html(item['href'])
2023-01-13 15:30:15 +00:00
continue
else:
# The name only needs to indicate that this is a buy link
for buy_str in buy_strings:
if buy_str in item_name.lower():
2023-07-12 08:53:32 +00:00
links[item_name] = remove_html(item['href'])
2023-01-13 15:30:15 +00:00
continue
2023-01-13 15:04:48 +00:00
return links
2023-01-13 15:16:08 +00:00
def load_buy_sites(base_dir: str) -> {}:
"""Loads domains from which buying is permitted
"""
2023-01-13 21:15:02 +00:00
buy_sites_filename = base_dir + '/accounts/buy_sites.json'
2023-01-13 15:16:08 +00:00
if os.path.isfile(buy_sites_filename):
buy_sites_json = load_json(buy_sites_filename)
if buy_sites_json:
return buy_sites_json
return {}