epicyon/webapp_profile.py

2958 lines
124 KiB
Python

__filename__ = "webapp_profile.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.4.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@libreserver.org"
__status__ = "Production"
__module_group__ = "Web Interface"
import os
from pprint import pprint
from webfinger import webfinger_handle
from utils import remove_id_ending
from utils import standardize_text
from utils import get_display_name
from utils import is_group_account
from utils import has_object_dict
from utils import get_occupation_name
from utils import get_locked_account
from utils import get_full_domain
from utils import is_artist
from utils import is_dormant
from utils import get_nickname_from_actor
from utils import get_domain_from_actor
from utils import is_system_account
from utils import remove_html
from utils import load_json
from utils import get_config_param
from utils import get_image_formats
from utils import acct_dir
from utils import get_supported_languages
from utils import local_actor_url
from utils import get_reply_interval_hours
from utils import get_account_timezone
from utils import remove_eol
from languages import get_actor_languages
from skills import get_skills
from theme import get_themes_list
from person import person_box_json
from person import get_actor_json
from person import get_person_avatar_url
from posts import get_post_expiry_keep_dms
from posts import get_post_expiry_days
from posts import get_person_box
from posts import is_moderator
from posts import parse_user_feed
from posts import is_create_inside_announce
from posts import get_max_profile_posts
from donate import get_donation_url
from donate import get_website
from donate import get_gemini_link
from xmpp import get_xmpp_address
from matrix import get_matrix_address
from ssb import get_ssb_address
from pgp import get_email_address
from pgp import get_pgp_fingerprint
from pgp import get_pgp_pub_key
from enigma import get_enigma_pub_key
from tox import get_tox_address
from briar import get_briar_address
from cwtch import get_cwtch_address
from filters import is_filtered
from follow import is_follower_of_person
from follow import get_follower_domains
from follow import is_following_actor
from webapp_frontscreen import html_front_screen
from webapp_utils import html_following_dropdown
from webapp_utils import edit_number_field
from webapp_utils import html_keyboard_navigation
from webapp_utils import html_hide_from_screen_reader
from webapp_utils import scheduled_posts_exist
from webapp_utils import html_header_with_external_style
from webapp_utils import html_header_with_person_markup
from webapp_utils import html_footer
from webapp_utils import add_emoji_to_display_name
from webapp_utils import get_profile_background_file
from webapp_utils import html_post_separator
from webapp_utils import edit_check_box
from webapp_utils import edit_text_field
from webapp_utils import edit_text_area
from webapp_utils import begin_edit_section
from webapp_utils import end_edit_section
from blog import get_blog_address
from webapp_post import individual_post_as_html
from webapp_timeline import html_individual_share
from webapp_timeline import page_number_buttons
from blocking import get_account_blocks
from blocking import get_cw_list_variable
from blocking import is_blocked
from content import bold_reading_string
from roles import is_devops
from session import site_is_verified
THEME_FORMATS = '.zip, .gz'
BLOCKFILE_FORMATS = '.csv'
def _valid_profile_preview_post(post_json_object: {},
person_url: str) -> (bool, {}):
"""Returns true if the given post should appear on a person/group profile
after searching for a handle
"""
if not isinstance(post_json_object, dict):
return False, None
is_announced_feed_item = False
if is_create_inside_announce(post_json_object):
is_announced_feed_item = True
post_json_object = post_json_object['object']
if not post_json_object.get('type'):
return False, None
if post_json_object['type'] == 'Create':
if not has_object_dict(post_json_object):
return False, None
if post_json_object['type'] != 'Create' and \
post_json_object['type'] != 'Announce':
if post_json_object['type'] != 'Note' and \
post_json_object['type'] != 'Page':
return False, None
if not post_json_object.get('to'):
return False, None
if not post_json_object.get('id'):
return False, None
# wrap in create
cc_list = []
if post_json_object.get('cc'):
cc_list = post_json_object['cc']
new_post_json_object = {
'object': post_json_object,
'to': post_json_object['to'],
'cc': cc_list,
'id': post_json_object['id'],
'actor': person_url,
'type': 'Create'
}
post_json_object = new_post_json_object
if not post_json_object.get('actor'):
return False, None
# convert actor back to id
if isinstance(post_json_object['actor'], dict):
if post_json_object['actor'].get('id'):
post_json_object['actor'] = post_json_object['actor']['id']
if has_object_dict(post_json_object):
# convert attributedTo actor back to id
if post_json_object['object'].get('attributedTo'):
if isinstance(post_json_object['object']['attributedTo'],
dict):
if post_json_object['object']['attributedTo'].get('id'):
post_json_object['object']['attributedTo'] = \
post_json_object['object']['attributedTo']['id']
if not is_announced_feed_item:
if post_json_object['actor'] != person_url and \
post_json_object['object']['type'] != 'Page':
return False, None
return True, post_json_object
def html_profile_after_search(recent_posts_cache: {}, max_recent_posts: int,
translate: {},
base_dir: str, path: str, http_prefix: str,
nickname: str, domain: str, port: int,
profile_handle: str,
session, cached_webfingers: {}, person_cache: {},
debug: bool, project_version: str,
yt_replace_domain: str,
twitter_replacement_domain: str,
show_published_date_only: bool,
default_timeline: str,
peertube_instances: [],
allow_local_network_access: bool,
theme_name: str,
access_keys: {},
system_language: str,
max_like_count: int,
signing_priv_key_pem: str,
cw_lists: {}, lists_enabled: str,
timezone: str,
onion_domain: str, i2p_domain: str,
bold_reading: bool, dogwhistles: {},
min_images_for_accounts: [],
buy_sites: {}) -> str:
"""Show a profile page after a search for a fediverse address
"""
http = False
gnunet = False
ipfs = False
ipns = False
if http_prefix == 'http':
http = True
elif http_prefix == 'gnunet':
gnunet = True
elif http_prefix == 'ipfs':
ipfs = True
elif http_prefix == 'ipns':
ipns = True
from_domain = domain
if onion_domain:
if '.onion/' in profile_handle or profile_handle.endswith('.onion'):
from_domain = onion_domain
http = True
if i2p_domain:
if '.i2p/' in profile_handle or profile_handle.endswith('.i2p'):
from_domain = i2p_domain
http = True
profile_json, as_header = \
get_actor_json(from_domain, profile_handle, http,
gnunet, ipfs, ipns, debug, False,
signing_priv_key_pem, session)
if not profile_json:
return None
if not profile_json.get('id'):
return None
person_url = profile_json['id']
search_domain, search_port = get_domain_from_actor(person_url)
if not search_domain:
return None
search_nickname = get_nickname_from_actor(person_url)
if not search_nickname:
return None
search_domain_full = get_full_domain(search_domain, search_port)
profile_str = ''
css_filename = base_dir + '/epicyon-profile.css'
if os.path.isfile(base_dir + '/epicyon.css'):
css_filename = base_dir + '/epicyon.css'
is_group = False
if profile_json.get('type'):
if profile_json['type'] == 'Group':
is_group = True
avatar_url = ''
if profile_json.get('icon'):
if profile_json['icon'].get('url'):
avatar_url = profile_json['icon']['url']
if not avatar_url:
avatar_url = get_person_avatar_url(base_dir, person_url, person_cache)
display_name = search_nickname
if profile_json.get('name'):
display_name = profile_json['name']
display_name = remove_html(display_name)
display_name = \
add_emoji_to_display_name(session, base_dir, http_prefix,
nickname, domain,
display_name, False, translate)
locked_account = get_locked_account(profile_json)
if locked_account:
display_name += '🔒'
moved_to = ''
if profile_json.get('movedTo'):
moved_to = profile_json['movedTo']
if '"' in moved_to:
moved_to = moved_to.split('"')[1]
display_name += ''
follows_you = \
is_follower_of_person(base_dir,
nickname, domain,
search_nickname,
search_domain_full)
profile_description = ''
if profile_json.get('summary'):
profile_description = profile_json['summary']
profile_description = \
add_emoji_to_display_name(session, base_dir, http_prefix,
nickname, domain,
profile_description, False, translate)
outbox_url = None
if not profile_json.get('outbox'):
if debug:
pprint(profile_json)
print('DEBUG: No outbox found')
return None
outbox_url = profile_json['outbox']
# profileBackgroundImage = ''
# if profile_json.get('image'):
# if profile_json['image'].get('url'):
# profileBackgroundImage = profile_json['image']['url']
# url to return to
back_url = path
if not back_url.endswith('/inbox'):
back_url += '/inbox'
profile_description_short = profile_description
if '\n' in profile_description:
if len(profile_description.split('\n')) > 2:
profile_description_short = ''
else:
if '<br>' in profile_description:
if len(profile_description.split('<br>')) > 2:
profile_description_short = ''
# keep the profile description short
if len(profile_description_short) > 2048:
profile_description_short = ''
# remove formatting from profile description used on title
avatar_description = ''
if profile_json.get('summary'):
if isinstance(profile_json['summary'], str):
avatar_description = \
profile_json['summary'].replace('<br>', '\n')
avatar_description = avatar_description.replace('<p>', '')
avatar_description = avatar_description.replace('</p>', '')
if '<' in avatar_description:
avatar_description = remove_html(avatar_description)
image_url = ''
if profile_json.get('image'):
if profile_json['image'].get('url'):
image_url = profile_json['image']['url']
also_known_as = None
if profile_json.get('alsoKnownAs'):
also_known_as = profile_json['alsoKnownAs']
joined_date = None
if profile_json.get('published'):
if 'T' in profile_json['published']:
joined_date = profile_json['published']
profile_str = \
_get_profile_header_after_search(nickname, default_timeline,
search_nickname,
search_domain_full,
translate,
display_name, follows_you,
profile_description_short,
avatar_url, image_url,
moved_to, profile_json['id'],
also_known_as, access_keys,
joined_date)
domain_full = get_full_domain(domain, port)
follow_is_permitted = True
if not profile_json.get('followers'):
# no followers collection specified within actor
follow_is_permitted = False
elif search_nickname == 'news' and search_domain_full == domain_full:
# currently the news actor is not something you can follow
follow_is_permitted = False
elif search_nickname == nickname and search_domain_full == domain_full:
# don't follow yourself!
follow_is_permitted = False
blocked = \
is_blocked(base_dir, nickname, domain, search_nickname, search_domain)
if follow_is_permitted:
follow_str = 'Follow'
if is_group:
follow_str = 'Join'
profile_str += \
'<div class="container">\n' + \
' <form method="POST" action="' + \
back_url + '/followconfirm">\n' + \
' <center>\n'
profile_str += \
' <input type="hidden" name="actor" value="' + \
person_url + '">\n'
if not is_following_actor(base_dir, nickname, domain, person_url):
if is_moderator(base_dir, nickname):
profile_str += \
' <button type="submit" class="button" ' + \
'name="submitInfo" ' + \
'accesskey="' + access_keys['infoButton'] + '">' + \
translate['Info'] + '</button>\n'
profile_str += \
' <button type="submit" class="button" ' + \
'name="submitYes" ' + \
'accesskey="' + access_keys['followButton'] + '">' + \
translate[follow_str] + '</button>\n'
profile_str += \
' <button type="submit" class="button" name="submitView" ' + \
'accesskey="' + access_keys['viewButton'] + '">' + \
translate['View'] + '</button>\n'
if blocked:
profile_str += \
' <button type="submit" ' + \
'class="button" name="submitUnblock" ' + \
'accesskey="' + access_keys['unblockButton'] + '">' + \
translate['Unblock'] + '</button>\n'
profile_str += \
' </center>\n' + \
' </form>\n' + \
'</div>\n'
else:
profile_str += \
'<div class="container">\n' + \
' <form method="POST" action="' + \
back_url + '/followconfirm">\n' + \
' <center>\n' + \
' <input type="hidden" name="actor" value="' + \
person_url + '">\n' + \
' <button type="submit" class="button" name="submitView" ' + \
'accesskey="' + access_keys['viewButton'] + '">' + \
translate['View'] + '</button>\n' + \
' </center>\n' + \
' </form>\n' + \
'</div>\n'
text_mode_separator = '<div class="transparent"><hr></div>'
user_feed = \
parse_user_feed(signing_priv_key_pem,
session, outbox_url, as_header, project_version,
http_prefix, from_domain, debug)
if user_feed:
minimize_all_images = False
if nickname in min_images_for_accounts:
minimize_all_images = True
i = 0
for item in user_feed:
show_item, post_json_object = \
_valid_profile_preview_post(item, person_url)
if not show_item:
if debug:
print('DEBUG: item not valid in profile posts: ' +
str(item))
continue
profile_str += \
text_mode_separator + \
individual_post_as_html(signing_priv_key_pem,
True, recent_posts_cache,
max_recent_posts,
translate, None, base_dir,
session, cached_webfingers,
person_cache,
nickname, domain, port,
post_json_object, avatar_url,
False, False,
http_prefix, project_version, 'inbox',
yt_replace_domain,
twitter_replacement_domain,
show_published_date_only,
peertube_instances,
allow_local_network_access,
theme_name, system_language,
max_like_count,
False, False, False,
False, False, False,
cw_lists, lists_enabled,
timezone, False,
bold_reading, dogwhistles,
minimize_all_images, None,
buy_sites)
i += 1
if i >= 8:
break
instance_title = get_config_param(base_dir, 'instanceTitle')
return html_header_with_external_style(css_filename,
instance_title, None) + \
profile_str + text_mode_separator + html_footer()
def _get_profile_header(base_dir: str, http_prefix: str, nickname: str,
domain: str, domain_full: str, translate: {},
default_timeline: str,
display_name: str,
profile_description_short: str,
login_button: str, avatar_url: str,
theme: str, moved_to: str,
also_known_as: [],
pinned_content: str,
access_keys: {},
joined_date: str,
occupation_name: str) -> str:
"""The header of the profile screen, containing background
image and avatar
"""
banner_file, _ = \
get_profile_background_file(base_dir, nickname, domain, theme)
html_str = \
'\n\n <figure class="profileHeader">\n' + \
' <a href="/users/' + \
nickname + '/' + default_timeline + '" title="' + \
translate['Switch to timeline view'] + '" tabindex="1" ' + \
'accesskey="' + access_keys['menuTimeline'] + '">\n' + \
' <img class="profileBackground" ' + \
'alt="" ' + \
'src="/users/' + nickname + '/' + banner_file + '" /></a>\n' + \
' <figcaption>\n' + \
' <a href="/users/' + \
nickname + '/' + default_timeline + '" title="' + \
translate['Switch to timeline view'] + '">\n' + \
' <img loading="lazy" decoding="async" ' + \
'src="' + avatar_url + '" alt="" class="title"></a>\n'
occupation_str = ''
if occupation_name:
occupation_str += \
' <b>' + occupation_name + '</b><br>\n'
html_str += \
' <h1>' + display_name + '\n</h1>\n' + \
occupation_str
html_str += \
' <p><b>@' + nickname + '@' + domain_full + '</b><br>\n'
if joined_date:
html_str += \
' <p>' + translate['Joined'] + ' ' + \
joined_date.split('T')[0] + '<br>\n'
if moved_to:
new_nickname = get_nickname_from_actor(moved_to)
new_domain, new_port = get_domain_from_actor(moved_to)
if new_nickname and new_domain:
new_domain_full = get_full_domain(new_domain, new_port)
html_str += \
' <p>' + translate['New account'] + ': ' + \
'<a href="' + moved_to + '">@' + \
new_nickname + '@' + new_domain_full + '</a><br>\n'
elif also_known_as:
other_accounts_html = \
' <p>' + translate['Other accounts'] + ': '
actor = local_actor_url(http_prefix, nickname, domain_full)
ctr = 0
if isinstance(also_known_as, list):
for alt_actor in also_known_as:
if alt_actor == actor:
continue
if ctr > 0:
other_accounts_html += ' '
ctr += 1
alt_domain, _ = get_domain_from_actor(alt_actor)
if alt_domain:
other_accounts_html += \
'<a href="' + alt_actor + \
'" tabindex="1">' + alt_domain + '</a>'
elif isinstance(also_known_as, str):
if also_known_as != actor:
ctr += 1
alt_domain, _ = get_domain_from_actor(also_known_as)
if alt_domain:
other_accounts_html += \
'<a href="' + also_known_as + '">' + \
alt_domain + '</a>'
other_accounts_html += '</p>\n'
if ctr > 0:
html_str += other_accounts_html
html_str += \
' <a href="/users/' + nickname + \
'/qrcode.png" alt="' + translate['QR Code'] + '" title="' + \
translate['QR Code'] + '" tabindex="1">' + \
'<img class="qrcode" alt="' + translate['QR Code'] + \
'" src="/icons/qrcode.png" /></a></p>\n' + \
' <p>' + profile_description_short + '</p>\n' + login_button
if pinned_content:
html_str += pinned_content.replace('<p>', '<p>📎', 1)
# show vcard download link
html_str += \
' <a href="/users/' + nickname + '.vcf" ' + \
'download="contact_' + nickname + '@' + domain_full + \
'.vcf" tabindex="1" class="imageAnchor">' + \
'<img class="vcard" src="/icons/vcard.png" ' + \
'title="vCard" alt="vCard" /></a>\n'
html_str += \
' </figcaption>\n' + \
' </figure>\n\n'
return html_str
def _get_profile_header_after_search(nickname: str, default_timeline: str,
search_nickname: str,
search_domain_full: str,
translate: {},
display_name: str,
follows_you: bool,
profile_description_short: str,
avatar_url: str, image_url: str,
moved_to: str, actor: str,
also_known_as: [],
access_keys: {},
joined_date: str) -> str:
"""The header of a searched for handle, containing background
image and avatar
"""
if not image_url:
image_url = '/defaultprofilebackground'
html_str = \
'\n\n <figure class="profileHeader">\n' + \
' <a href="/users/' + \
nickname + '/' + default_timeline + '" title="' + \
translate['Switch to timeline view'] + '" ' + \
'accesskey="' + access_keys['menuTimeline'] + '" tabindex="1">\n' + \
' <img class="profileBackground" ' + \
'alt="" ' + \
'src="' + image_url + '" /></a>\n' + \
' <figcaption>\n'
if avatar_url:
html_str += \
' <a href="/users/' + \
nickname + '/' + default_timeline + '" title="' + \
translate['Switch to timeline view'] + '">\n' + \
' <img loading="lazy" decoding="async" src="' + \
avatar_url + '" ' + 'alt="" class="title"></a>\n'
if not display_name:
display_name = search_nickname
html_str += \
' <h1>\n' + \
' ' + display_name + '\n' + \
' </h1>\n' + \
' <p><b>@' + search_nickname + '@' + search_domain_full + \
'</b><br>\n'
if joined_date:
html_str += ' <p>' + translate['Joined'] + ' ' + \
joined_date.split('T')[0] + '</p>\n'
if follows_you:
html_str += ' <p><b>' + translate['Follows you'] + '</b></p>\n'
if moved_to:
new_nickname = get_nickname_from_actor(moved_to)
new_domain, new_port = get_domain_from_actor(moved_to)
if new_nickname and new_domain:
new_domain_full = get_full_domain(new_domain, new_port)
new_handle = new_nickname + '@' + new_domain_full
html_str += ' <p>' + translate['New account'] + \
': <a href="' + moved_to + '">@' + new_handle + '</a></p>\n'
elif also_known_as:
other_accounts_html = \
' <p>' + translate['Other accounts'] + ': '
ctr = 0
if isinstance(also_known_as, list):
for alt_actor in also_known_as:
if alt_actor == actor:
continue
if ctr > 0:
other_accounts_html += ' '
ctr += 1
alt_domain, _ = get_domain_from_actor(alt_actor)
if alt_domain:
other_accounts_html += \
'<a href="' + alt_actor + \
'" tabindex="1">' + alt_domain + '</a>'
elif isinstance(also_known_as, str):
if also_known_as != actor:
ctr += 1
alt_domain, _ = get_domain_from_actor(also_known_as)
if alt_domain:
other_accounts_html += \
'<a href="' + also_known_as + '">' + \
alt_domain + '</a>'
other_accounts_html += '</p>\n'
if ctr > 0:
html_str += other_accounts_html
html_str += \
' <p>' + profile_description_short + '</p>\n' + \
' </figcaption>\n' + \
' </figure>\n\n'
return html_str
def html_profile(signing_priv_key_pem: str,
rss_icon_at_top: bool,
icons_as_buttons: bool,
default_timeline: str,
recent_posts_cache: {}, max_recent_posts: int,
translate: {}, project_version: str,
base_dir: str, http_prefix: str, authorized: bool,
profile_json: {}, selected: str,
session, cached_webfingers: {}, person_cache: {},
yt_replace_domain: str,
twitter_replacement_domain: str,
show_published_date_only: bool,
newswire: {}, theme: str, dormant_months: int,
peertube_instances: [],
allow_local_network_access: bool,
text_mode_banner: str,
debug: bool, access_keys: {}, city: str,
system_language: str, max_like_count: int,
shared_items_federated_domains: [],
extra_json: {}, page_number: int,
max_items_per_page: int,
cw_lists: {}, lists_enabled: str,
content_license_url: str,
timezone: str, bold_reading: bool,
buy_sites: {}) -> str:
"""Show the profile page as html
"""
show_moved_accounts = False
if authorized:
moved_accounts_filename = base_dir + '/accounts/actors_moved.txt'
if os.path.isfile(moved_accounts_filename):
show_moved_accounts = True
nickname = profile_json['preferredUsername']
if not nickname:
return ""
if is_system_account(nickname):
min_images_for_accounts = []
return html_front_screen(signing_priv_key_pem,
rss_icon_at_top,
icons_as_buttons,
default_timeline,
recent_posts_cache, max_recent_posts,
translate, project_version,
base_dir, http_prefix, authorized,
profile_json, selected,
session, cached_webfingers, person_cache,
yt_replace_domain,
twitter_replacement_domain,
show_published_date_only,
newswire, theme, extra_json,
allow_local_network_access, access_keys,
system_language, max_like_count,
shared_items_federated_domains, None,
page_number, max_items_per_page, cw_lists,
lists_enabled, {},
min_images_for_accounts, buy_sites)
domain, port = get_domain_from_actor(profile_json['id'])
if not domain:
return ""
display_name = remove_html(profile_json['name'])
display_name = \
add_emoji_to_display_name(session, base_dir, http_prefix,
nickname, domain,
display_name, False, translate)
domain_full = get_full_domain(domain, port)
profile_description = profile_json['summary']
profile_description = \
add_emoji_to_display_name(session, base_dir, http_prefix,
nickname, domain,
profile_description, False, translate)
if profile_description:
profile_description = standardize_text(profile_description)
posts_button = 'button'
following_button = 'button'
moved_button = 'button'
moved_button = 'button'
inactive_button = 'button'
followers_button = 'button'
roles_button = 'button'
skills_button = 'button'
# shares_button = 'button'
# wanted_button = 'button'
if selected == 'posts':
posts_button = 'buttonselected'
elif selected == 'following':
following_button = 'buttonselected'
elif selected == 'moved':
moved_button = 'buttonselected'
elif selected == 'inactive':
inactive_button = 'buttonselected'
elif selected == 'followers':
followers_button = 'buttonselected'
elif selected == 'roles':
roles_button = 'buttonselected'
elif selected == 'skills':
skills_button = 'buttonselected'
# elif selected == 'shares':
# shares_button = 'buttonselected'
# elif selected == 'wanted':
# wanted_button = 'buttonselected'
login_button = ''
follow_approvals_section = ''
follow_approvals = False
edit_profile_str = ''
logout_str = ''
actor = profile_json['id']
users_path = '/users/' + actor.split('/users/')[1]
donate_section = ''
donate_url = get_donation_url(profile_json)
website_url = get_website(profile_json, translate)
gemini_link = get_gemini_link(profile_json, translate)
blog_address = get_blog_address(profile_json)
enigma_pub_key = get_enigma_pub_key(profile_json)
pgp_pub_key = get_pgp_pub_key(profile_json)
pgp_fingerprint = get_pgp_fingerprint(profile_json)
email_address = get_email_address(profile_json)
xmpp_address = get_xmpp_address(profile_json)
matrix_address = get_matrix_address(profile_json)
ssb_address = get_ssb_address(profile_json)
tox_address = get_tox_address(profile_json)
briar_address = get_briar_address(profile_json)
cwtch_address = get_cwtch_address(profile_json)
verified_site_checkmark = ''
if donate_url or website_url or xmpp_address or matrix_address or \
ssb_address or tox_address or briar_address or \
cwtch_address or pgp_pub_key or enigma_pub_key or \
pgp_fingerprint or email_address:
donate_section = '<div class="container">\n'
donate_section += ' <center>\n'
if donate_url and not is_system_account(nickname):
donate_section += \
' <p><a href="' + donate_url + '" tabindex="1">' + \
'<button class="donateButton">' + translate['Donate'] + \
'</button></a></p>\n'
if website_url:
if site_is_verified(session, base_dir, http_prefix,
nickname, domain,
website_url, False, debug):
donate_section += \
'<p><div class="verified_site">' + \
translate['Website'] + ': ' + \
verified_site_checkmark + '<a href="' + \
website_url + '" rel="me" tabindex="1">' + \
website_url + '</a></div></p>\n'
else:
donate_section += \
'<p>' + translate['Website'] + ': ' + \
'<a href="' + \
website_url + '" rel="me" tabindex="1">' + \
website_url + '</a></p>\n'
if gemini_link:
donate_section += \
'<p>' + 'Gemini' + ': <a href="' + \
gemini_link + '" tabindex="1">' + \
gemini_link + '</a></p>\n'
if email_address:
donate_section += \
'<p>' + translate['Email'] + ': <a href="mailto:' + \
email_address + '" tabindex="1">' + \
email_address + '</a></p>\n'
if blog_address:
if site_is_verified(session, base_dir, http_prefix,
nickname, domain,
blog_address, False, debug):
donate_section += \
'<p><div class="verified_site">' + \
'Blog: ' + verified_site_checkmark + \
'<a href="' + \
blog_address + '" rel="me" tabindex="1">' + \
blog_address + '</a></div></p>\n'
else:
donate_section += \
'<p>Blog: <a href="' + \
blog_address + '" rel="me" tabindex="1">' + \
blog_address + '</a></p>\n'
if xmpp_address:
donate_section += \
'<p>' + translate['XMPP'] + ': <a href="xmpp:' + \
xmpp_address + '" tabindex="1">' + xmpp_address + '</a></p>\n'
if matrix_address:
donate_section += \
'<p>' + translate['Matrix'] + ': ' + matrix_address + '</p>\n'
if ssb_address:
donate_section += \
'<p>SSB: <label class="ssbaddr">' + \
ssb_address + '</label></p>\n'
if tox_address:
donate_section += \
'<p>Tox: <label class="toxaddr">' + \
tox_address + '</label></p>\n'
if briar_address:
if briar_address.startswith('briar://'):
donate_section += \
'<p><label class="toxaddr">' + \
briar_address + '</label></p>\n'
else:
donate_section += \
'<p>briar://<label class="toxaddr">' + \
briar_address + '</label></p>\n'
if cwtch_address:
donate_section += \
'<p>Cwtch: <label class="toxaddr">' + \
cwtch_address + '</label></p>\n'
if enigma_pub_key:
donate_section += \
'<p>Enigma: <label class="toxaddr">' + \
enigma_pub_key + '</label></p>\n'
if pgp_fingerprint:
donate_section += \
'<p class="pgp">PGP: ' + \
pgp_fingerprint.replace('\n', '<br>') + '</p>\n'
if pgp_pub_key:
donate_section += \
'<p class="pgp">' + \
pgp_pub_key.replace('\n', '<br>') + '</p>\n'
donate_section += ' </center>\n'
donate_section += '</div>\n'
if authorized:
edit_profile_str = \
'<a class="imageAnchor" href="' + users_path + \
'/editprofile" tabindex="1">' + \
'<img loading="lazy" decoding="async" src="/icons' + \
'/edit.png" title="' + translate['Edit'] + \
'" alt="| ' + translate['Edit'] + '" class="timelineicon"/></a>\n'
logout_str = \
'<a class="imageAnchor" href="/logout" tabindex="1">' + \
'<img loading="lazy" decoding="async" src="/icons' + \
'/logout.png" title="' + translate['Logout'] + \
'" alt="| ' + translate['Logout'] + \
'" class="timelineicon"/></a>\n'
# are there any follow requests?
follow_requests_filename = \
acct_dir(base_dir, nickname, domain) + '/followrequests.txt'
if os.path.isfile(follow_requests_filename):
with open(follow_requests_filename, 'r',
encoding='utf-8') as foll_file:
for line in foll_file:
if len(line) > 0:
follow_approvals = True
followers_button = 'buttonhighlighted'
if selected == 'followers':
followers_button = 'buttonselectedhighlighted'
break
if selected == 'followers':
if follow_approvals:
curr_follower_domains = \
get_follower_domains(base_dir, nickname, domain)
with open(follow_requests_filename, 'r',
encoding='utf-8') as req_file:
for follower_handle in req_file:
if len(follower_handle) > 0:
follower_handle = \
remove_eol(follower_handle)
if '://' in follower_handle:
follower_actor = follower_handle
else:
nick = follower_handle.split('@')[0]
dom = follower_handle.split('@')[1]
follower_actor = \
local_actor_url(http_prefix, nick, dom)
# is this a new domain?
# if so then append a new instance indicator
follower_domain, _ = \
get_domain_from_actor(follower_actor)
new_follower_domain = ''
if follower_domain not in curr_follower_domains:
new_follower_domain = ''
# Show the handle of the potential follower
# being approved, linking to search on that handle
base_path = '/users/' + nickname
follow_approvals_section += \
'<div class="container">\n' + \
' <form method="POST" action="' + \
base_path + '/searchhandle?page=1">\n' + \
' <input type="hidden" ' + \
'name="actor" value="' + actor + '">\n' + \
' <input type="hidden" ' + \
'name="searchtext" value="' + \
follower_actor + \
'">\n <button type="submit" ' + \
'class="followApproveHandle" ' + \
'name="submitSearch" tabindex="2">' + \
follower_handle + new_follower_domain + \
'</button>\n </form>\n'
# show Approve and Deny buttons
follow_approvals_section += \
'<a href="' + base_path + \
'/followapprove=' + follower_handle + \
'" tabindex="2">'
follow_approvals_section += \
'<button class="followApprove">' + \
translate['Approve'] + '</button></a><br><br>'
follow_approvals_section += \
'<a href="' + base_path + \
'/followdeny=' + follower_handle + \
'" tabindex="3">'
follow_approvals_section += \
'<button class="followDeny">' + \
translate['Deny'] + '</button></a>'
follow_approvals_section += '</div>'
profile_description_short = profile_description
if '\n' in profile_description:
if len(profile_description.split('\n')) > 2:
profile_description_short = ''
else:
if '<br>' in profile_description:
if len(profile_description.split('<br>')) > 2:
profile_description_short = ''
profile_description = profile_description.replace('<br>', '\n')
# keep the profile description short
if len(profile_description_short) > 2048:
profile_description_short = ''
# remove formatting from profile description used on title
avatar_description = ''
if profile_json.get('summary'):
avatar_description = profile_json['summary'].replace('<br>', '\n')
avatar_description = avatar_description.replace('<p>', '')
avatar_description = avatar_description.replace('</p>', '')
moved_to = ''
if profile_json.get('movedTo'):
moved_to = profile_json['movedTo']
if '"' in moved_to:
moved_to = moved_to.split('"')[1]
also_known_as = None
if profile_json.get('alsoKnownAs'):
also_known_as = profile_json['alsoKnownAs']
joined_date = None
if profile_json.get('published'):
if 'T' in profile_json['published']:
joined_date = profile_json['published']
occupation_name = None
if profile_json.get('hasOccupation'):
occupation_name = get_occupation_name(profile_json)
avatar_url = profile_json['icon']['url']
# use alternate path for local avatars to avoid any caching issues
if '://' + domain_full + '/system/accounts/avatars/' in avatar_url:
avatar_url = \
avatar_url.replace('://' + domain_full +
'/system/accounts/avatars/',
'://' + domain_full + '/users/')
# get pinned post content
account_dir = acct_dir(base_dir, nickname, domain)
pinned_filename = account_dir + '/pinToProfile.txt'
pinned_content = None
if os.path.isfile(pinned_filename):
with open(pinned_filename, 'r', encoding='utf-8') as pin_file:
pinned_content = pin_file.read()
profile_header_str = \
_get_profile_header(base_dir, http_prefix,
nickname,
domain, domain_full, translate,
default_timeline, display_name,
profile_description_short,
login_button, avatar_url, theme,
moved_to, also_known_as,
pinned_content, access_keys,
joined_date, occupation_name)
# keyboard navigation
user_path_str = '/users/' + nickname
deft = default_timeline
is_group = False
followers_str = translate['Followers']
if is_group_account(base_dir, nickname, domain):
is_group = True
followers_str = translate['Members']
menu_timeline = \
html_hide_from_screen_reader('🏠') + ' ' + \
translate['Switch to timeline view']
menu_edit = \
html_hide_from_screen_reader('') + ' ' + translate['Edit']
menu_followers = \
html_hide_from_screen_reader('👪') + ' ' + followers_str
if show_moved_accounts:
menu_moved = \
html_hide_from_screen_reader('') + ' ' + translate['Moved']
menu_inactive = \
html_hide_from_screen_reader('💤') + ' ' + translate['Inactive']
menu_logout = \
html_hide_from_screen_reader('') + ' ' + translate['Logout']
if not show_moved_accounts:
nav_links = {
menu_timeline: user_path_str + '/' + deft,
menu_edit: user_path_str + '/editprofile',
menu_followers: user_path_str + '/followers#timeline',
menu_logout: '/logout'
}
else:
nav_links = {
menu_timeline: user_path_str + '/' + deft,
menu_edit: user_path_str + '/editprofile',
menu_followers: user_path_str + '/followers#timeline',
menu_moved: user_path_str + '/moved#timeline',
menu_inactive: user_path_str + '/inactive#timeline',
menu_logout: '/logout'
}
if not is_group:
menu_following = \
html_hide_from_screen_reader('👥') + ' ' + translate['Following']
nav_links[menu_following] = user_path_str + '/following#timeline'
menu_roles = \
html_hide_from_screen_reader('🤚') + ' ' + translate['Roles']
nav_links[menu_roles] = user_path_str + '/roles#timeline'
menu_skills = \
html_hide_from_screen_reader('🛠') + ' ' + translate['Skills']
nav_links[menu_skills] = user_path_str + '/skills#timeline'
if is_artist(base_dir, nickname):
menu_theme_designer = \
html_hide_from_screen_reader('🎨') + ' ' + \
translate['Theme Designer']
nav_links[menu_theme_designer] = user_path_str + '/themedesigner'
nav_access_keys = {}
for variable_name, key in access_keys.items():
if not locals().get(variable_name):
continue
nav_access_keys[locals()[variable_name]] = key
profile_str = html_keyboard_navigation(text_mode_banner,
nav_links, nav_access_keys)
profile_str += profile_header_str + donate_section
profile_str += '<div class="container" id="buttonheader">\n'
profile_str += ' <center>'
profile_str += \
' <a href="' + users_path + '#buttonheader" tabindex="2">' + \
'<button class="' + \
posts_button + '"><span>' + translate['Posts'] + \
' </span></button></a>'
if not is_group:
profile_str += \
' <a href="' + users_path + \
'/following#buttonheader" tabindex="2">' + \
'<button class="' + following_button + '"><span>' + \
translate['Following'] + ' </span></button></a>'
profile_str += \
' <a href="' + users_path + \
'/followers#buttonheader" tabindex="2">' + \
'<button class="' + followers_button + \
'"><span>' + followers_str + ' </span></button></a>'
profile_str += \
' <a href="' + users_path + \
'/inactive#buttonheader" tabindex="2">' + \
'<button class="' + inactive_button + \
'"><span>' + translate['Inactive'] + ' </span></button></a>'
if not is_group:
if show_moved_accounts:
profile_str += \
' <a href="' + users_path + \
'/moved#buttonheader" tabindex="2">' + \
'<button class="' + moved_button + '"><span>' + \
translate['Moved'] + ' </span></button></a>'
profile_str += \
' <a href="' + users_path + \
'/roles#buttonheader" tabindex="2">' + \
'<button class="' + roles_button + '"><span>' + \
translate['Roles'] + \
' </span></button></a>'
profile_str += \
' <a href="' + users_path + \
'/skills#buttonheader" tabindex="2">' + \
'<button class="' + skills_button + '"><span>' + \
translate['Skills'] + ' </span></button></a>'
# profile_str += \
# ' <a href="' + users_path + \
# '/shares#buttonheader" tabindex="2">' + \
# '<button class="' + shares_button + '"><span>' + \
# translate['Shares'] + ' </span></button></a>'
# profile_str += \
# ' <a href="' + users_path + \
# '/wanted#buttonheader" tabindex="2">' + \
# '<button class="' + wanted_button + '"><span>' + \
# translate['Wanted'] + ' </span></button></a>'
profile_str += logout_str + edit_profile_str
profile_str += ' </center>'
profile_str += '</div>'
# search for following or followers
if authorized:
if selected in ('following', 'followers'):
follow_search_str = '<div class="container">\n'
follow_search_str += \
'<form method="POST" action="' + users_path + \
'/searchhandle?page=1">\n'
follow_search_str += \
' <input type="hidden" ' + \
'name="actor" value="' + actor + '">\n'
follow_search_str += \
html_following_dropdown(base_dir, nickname, domain,
domain_full, selected, False)
follow_search_str += \
' <button type="submit" class="button" ' + \
'name="submitSearch">' + translate['View'] + '</button>\n'
follow_search_str += '</form>\n</div>\n'
profile_str += follow_search_str
# start of #timeline
profile_str += '<div id="timeline">\n'
profile_str += follow_approvals_section
css_filename = base_dir + '/epicyon-profile.css'
if os.path.isfile(base_dir + '/epicyon.css'):
css_filename = base_dir + '/epicyon.css'
license_str = \
'<a href="https://gitlab.com/bashrc2/epicyon" tabindex="2">' + \
'<img loading="lazy" decoding="async" class="license" alt="' + \
translate['Get the source code'] + '" title="' + \
translate['Get the source code'] + '" src="/icons/agpl.png" /></a>'
if selected == 'posts':
max_profile_posts = \
get_max_profile_posts(base_dir, nickname, domain, 20)
min_images_for_accounts = []
profile_str += \
_html_profile_posts(recent_posts_cache, max_profile_posts,
translate,
base_dir, http_prefix, authorized,
nickname, domain, port,
session, cached_webfingers, person_cache,
project_version,
yt_replace_domain,
twitter_replacement_domain,
show_published_date_only,
peertube_instances,
allow_local_network_access,
theme, system_language,
max_like_count,
signing_priv_key_pem,
cw_lists, lists_enabled,
timezone, bold_reading, {},
min_images_for_accounts,
max_profile_posts,
buy_sites) + license_str
if not is_group:
if selected == 'following':
profile_str += \
_html_profile_following(translate, base_dir, http_prefix,
authorized, nickname,
domain, session,
cached_webfingers,
person_cache, extra_json,
project_version, ["unfollow"],
selected,
users_path, page_number,
max_items_per_page,
dormant_months, debug,
signing_priv_key_pem)
if show_moved_accounts and selected == 'moved':
profile_str += \
_html_profile_following(translate, base_dir, http_prefix,
authorized, nickname,
domain, session,
cached_webfingers,
person_cache, extra_json,
project_version, ["moveAccount"],
selected,
users_path, page_number,
max_items_per_page,
dormant_months, debug,
signing_priv_key_pem)
if selected == 'followers':
profile_str += \
_html_profile_following(translate, base_dir, http_prefix,
authorized, nickname,
domain, session,
cached_webfingers,
person_cache, extra_json,
project_version, ["block"],
selected, users_path, page_number,
max_items_per_page, dormant_months, debug,
signing_priv_key_pem)
if authorized and selected == 'inactive':
profile_str += \
_html_profile_following(translate, base_dir, http_prefix,
authorized, nickname,
domain, session,
cached_webfingers,
person_cache, extra_json,
project_version, ["block"],
selected, users_path, page_number,
max_items_per_page, dormant_months, debug,
signing_priv_key_pem)
if not is_group:
if selected == 'roles':
profile_str += \
_html_profile_roles(translate, nickname, domain_full,
extra_json)
elif selected == 'skills':
profile_str += \
_html_profile_skills(extra_json)
# elif selected == 'shares':
# profile_str += \
# _html_profile_shares(actor, translate,
# domain_full,
# extra_json, 'shares') + license_str
# elif selected == 'wanted':
# profile_str += \
# _html_profile_shares(actor, translate,
# domain_full,
# extra_json, 'wanted') + license_str
# end of #timeline
profile_str += '</div>\n<br>\n'
instance_title = \
get_config_param(base_dir, 'instanceTitle')
profile_str = \
html_header_with_person_markup(css_filename, instance_title,
profile_json, city,
content_license_url) + \
profile_str + html_footer()
return profile_str
def _html_profile_posts(recent_posts_cache: {}, max_recent_posts: int,
translate: {},
base_dir: str, http_prefix: str,
authorized: bool,
nickname: str, domain: str, port: int,
session, cached_webfingers: {}, person_cache: {},
project_version: str,
yt_replace_domain: str,
twitter_replacement_domain: str,
show_published_date_only: bool,
peertube_instances: [],
allow_local_network_access: bool,
theme_name: str, system_language: str,
max_like_count: int,
signing_priv_key_pem: str,
cw_lists: {}, lists_enabled: str,
timezone: str, bold_reading: bool,
dogwhistles: {},
min_images_for_accounts: [],
max_profile_posts: int,
buy_sites: {}) -> str:
"""Shows posts on the profile screen
These should only be public posts
"""
separator_str = html_post_separator(base_dir, None)
profile_str = ''
max_items = max_profile_posts
ctr = 0
curr_page = 1
box_name = 'outbox'
minimize_all_images = False
if nickname in min_images_for_accounts:
minimize_all_images = True
while ctr < max_items and curr_page < 4:
outbox_feed_path_str = \
'/users/' + nickname + '/' + box_name + '?page=' + \
str(curr_page)
outbox_feed = \
person_box_json({}, base_dir, domain,
port,
outbox_feed_path_str,
http_prefix,
10, box_name,
authorized, 0, False, 0)
if not outbox_feed:
break
if len(outbox_feed['orderedItems']) == 0:
break
shown_items = []
for item in outbox_feed['orderedItems']:
if item['type'] == 'Create':
if not item['object'].get('id'):
continue
item_id = remove_id_ending(item['object']['id'])
post_str = \
individual_post_as_html(signing_priv_key_pem,
True, recent_posts_cache,
max_recent_posts,
translate, None,
base_dir, session,
cached_webfingers,
person_cache,
nickname, domain, port, item,
None, True, False,
http_prefix, project_version,
'inbox',
yt_replace_domain,
twitter_replacement_domain,
show_published_date_only,
peertube_instances,
allow_local_network_access,
theme_name, system_language,
max_like_count,
False, False, False,
True, False, False,
cw_lists, lists_enabled,
timezone, False,
bold_reading, dogwhistles,
minimize_all_images, None,
buy_sites)
if post_str and item_id not in shown_items:
profile_str += post_str + separator_str
shown_items.append(item_id)
ctr += 1
if ctr >= max_items:
break
curr_page += 1
return profile_str
def _html_profile_following(translate: {}, base_dir: str, http_prefix: str,
authorized: bool, nickname: str, domain: str,
session, cached_webfingers: {}, person_cache: {},
following_json: {}, project_version: str,
buttons: [],
feed_name: str, actor: str,
page_number: int,
max_items_per_page: int,
dormant_months: int, debug: bool,
signing_priv_key_pem: str) -> str:
"""Shows following on the profile screen
"""
profile_str = ''
if authorized and page_number:
if authorized and page_number > 1:
# page up arrow
profile_str += \
' <center>\n' + \
' <a href="' + actor + '/' + feed_name + \
'?page=' + str(page_number - 1) + '#buttonheader' + \
'"><img loading="lazy" decoding="async" ' + \
'class="pageicon" src="/' + \
'icons/pageup.png" title="' + \
translate['Page up'] + '" alt="' + \
translate['Page up'] + '"></a>\n' + \
' </center>\n'
for following_actor in following_json['orderedItems']:
# is this a dormant followed account?
dormant = False
if authorized and feed_name == 'following':
dormant = \
is_dormant(base_dir, nickname, domain, following_actor,
dormant_months)
profile_str += \
_individual_follow_as_html(signing_priv_key_pem,
translate, base_dir, session,
cached_webfingers, person_cache,
domain, following_actor,
authorized, nickname,
http_prefix, project_version, dormant,
debug, buttons)
if authorized and max_items_per_page and page_number:
if len(following_json['orderedItems']) >= max_items_per_page:
# page down arrow
profile_str += \
' <center>\n' + \
' <a href="' + actor + '/' + feed_name + \
'?page=' + str(page_number + 1) + '#buttonheader' + \
'"><img loading="lazy" decoding="async" ' + \
'class="pageicon" src="/' + \
'icons/pagedown.png" title="' + \
translate['Page down'] + '" alt="' + \
translate['Page down'] + '"></a>\n' + \
' </center>\n'
# list of page numbers
profile_str += \
page_number_buttons(actor, feed_name, page_number,
'buttonheader')
# some vertical padding to allow "finger space" on mobile
profile_str += '<br>'
return profile_str
def _html_profile_roles(translate: {}, nickname: str, domain: str,
roles_list: []) -> str:
"""Shows roles on the profile screen
"""
profile_str = ''
profile_str += \
'<div class="roles">\n<div class="roles-inner">\n'
for role in roles_list:
if translate.get(role):
profile_str += '<h3>' + translate[role] + '</h3>\n'
else:
profile_str += '<h3>' + role + '</h3>\n'
profile_str += '</div></div>\n'
if len(profile_str) == 0:
profile_str += \
'<p>@' + nickname + '@' + domain + ' has no roles assigned</p>\n'
else:
profile_str = '<div>' + profile_str + '</div>\n'
return profile_str
def _html_profile_skills(skills_json: {}) -> str:
"""Shows skills on the profile screen
"""
profile_str = ''
for skill, level in skills_json.items():
profile_str += \
'<div>' + skill + \
'<br><div id="myProgress"><div id="myBar" style="width:' + \
str(level) + '%"></div></div></div>\n<br>\n'
if len(profile_str) > 0:
profile_str = '<center><div class="skill-title">' + \
profile_str + '</div></center>\n'
return profile_str
def _html_profile_shares(actor: str, translate: {},
domain: str, shares_json: {},
shares_file_type: str) -> str:
"""Shows shares on the profile screen
"""
profile_str = ''
for item in shares_json['orderedItems']:
profile_str += html_individual_share(domain, item['shareId'],
actor, item, translate,
False, False,
shares_file_type)
if len(profile_str) > 0:
profile_str = '<div class="share-title">' + profile_str + '</div>\n'
return profile_str
def _grayscale_enabled(base_dir: str) -> bool:
"""Is grayscale UI enabled?
"""
return os.path.isfile(base_dir + '/accounts/.grayscale')
def _html_themes_dropdown(base_dir: str, translate: {}) -> str:
"""Returns the html for theme selection dropdown
"""
# Themes section
themes = get_themes_list(base_dir)
themes_dropdown = ' <label class="labels">' + \
translate['Theme'] + '</label><br>\n'
grayscale = _grayscale_enabled(base_dir)
themes_dropdown += \
edit_check_box(translate['Grayscale'], 'grayscale', grayscale)
dyslexic_font = get_config_param(base_dir, 'dyslexicFont')
themes_dropdown += \
edit_check_box(translate['Dyslexic font'], 'dyslexicFont',
dyslexic_font)
themes_dropdown += ' <select id="themeDropdown" ' + \
'name="themeDropdown" class="theme">'
for theme_name in themes:
translated_theme_name = theme_name
if translate.get(theme_name):
translated_theme_name = translate[theme_name]
themes_dropdown += ' <option value="' + \
theme_name.lower() + '">' + \
translated_theme_name + '</option>'
themes_dropdown += ' </select><br>'
if os.path.isfile(base_dir + '/fonts/custom.woff') or \
os.path.isfile(base_dir + '/fonts/custom.woff2') or \
os.path.isfile(base_dir + '/fonts/custom.otf') or \
os.path.isfile(base_dir + '/fonts/custom.ttf'):
themes_dropdown += \
edit_check_box(translate['Remove the custom font'],
'removeCustomFont', False)
theme_name = get_config_param(base_dir, 'theme')
themes_dropdown = \
themes_dropdown.replace('<option value="' + theme_name + '">',
'<option value="' + theme_name +
'" selected>')
return themes_dropdown
def _html_edit_profile_graphic_design(base_dir: str, translate: {}) -> str:
"""Graphic design section on Edit Profile screen
"""
graphics_str = begin_edit_section(translate['Graphic Design'])
low_bandwidth = get_config_param(base_dir, 'lowBandwidth')
if not low_bandwidth:
low_bandwidth = False
graphics_str += _html_themes_dropdown(base_dir, translate)
graphics_str += \
' <label class="labels">' + \
translate['Import Theme'] + '</label>\n'
graphics_str += ' <input type="file" id="importTheme" '
graphics_str += 'name="importTheme" '
graphics_str += 'accept="' + THEME_FORMATS + '">\n'
graphics_str += \
' <label class="labels">' + \
translate['Export Theme'] + '</label><br>\n'
graphics_str += \
' <button type="submit" class="button" ' + \
'name="submitExportTheme">➤</button><br>\n'
graphics_str += \
edit_check_box(translate['Low Bandwidth'], 'lowBandwidth',
bool(low_bandwidth))
graphics_str += end_edit_section()
return graphics_str
def _html_edit_profile_twitter(base_dir: str, translate: {},
remove_twitter: str) -> str:
"""Edit twitter settings within profile
"""
# Twitter section
twitter_str = begin_edit_section(translate['Twitter'])
twitter_str += \
edit_check_box(translate['Remove Twitter posts'],
'removeTwitter', remove_twitter)
twitter_replacement_domain = get_config_param(base_dir, "twitterdomain")
if not twitter_replacement_domain:
twitter_replacement_domain = ''
twitter_str += \
edit_text_field(translate['Twitter Replacement Domain'],
'twitterdomain', twitter_replacement_domain)
twitter_str += end_edit_section()
return twitter_str
def _html_edit_profile_instance(base_dir: str, translate: {},
peertube_instances: [],
media_instance_str: str,
blogs_instance_str: str,
news_instance_str: str) -> (str, str,
str, str):
"""Edit profile instance settings
"""
image_formats = get_image_formats()
# Instance details section
instance_description = \
get_config_param(base_dir, 'instanceDescription')
custom_submit_text = \
get_config_param(base_dir, 'customSubmitText')
instance_description_short = \
get_config_param(base_dir, 'instanceDescriptionShort')
instance_title = \
get_config_param(base_dir, 'instanceTitle')
content_license_url = \
get_config_param(base_dir, 'contentLicenseUrl')
if not content_license_url:
content_license_url = 'https://creativecommons.org/licenses/by-nc/4.0'
instance_str = begin_edit_section(translate['Instance Settings'])
instance_str += \
edit_text_field(translate['Instance Title'],
'instanceTitle', instance_title)
instance_str += '<br>\n'
instance_str += \
edit_text_field(translate['Instance Short Description'],
'instanceDescriptionShort', instance_description_short)
instance_str += '<br>\n'
instance_str += \
edit_text_area(translate['Instance Description'], None,
'instanceDescription', instance_description, 200,
'', True)
instance_str += \
edit_text_field(translate['Content License'],
'contentLicenseUrl', content_license_url)
instance_str += '<br>\n'
instance_str += \
edit_text_field(translate['Custom post submit button text'],
'customSubmitText', custom_submit_text)
instance_str += '<br>\n'
instance_str += \
' <label class="labels">' + \
translate['Instance Logo'] + '</label>' + \
' <input type="file" id="instanceLogo" name="instanceLogo"' + \
' accept="' + image_formats + '"><br>\n <br>\n'
registrations_open = False
if get_config_param(base_dir, "registration") == 'open':
registrations_open = True
instance_str += \
edit_check_box(translate['Registrations open'],
'regOpen', registrations_open)
if registrations_open:
remaining_config_exists = \
get_config_param(base_dir, 'registrationsRemaining')
registrations_remaining = 0
if remaining_config_exists is not None:
registrations_remaining = int(remaining_config_exists)
instance_str += \
edit_number_field(translate['Registrations remaining'],
'regRemaining',
registrations_remaining, 0, 10, 10)
instance_str += ' <br>\n'
instance_str += \
' <label class="labels">\n' + \
translate['Security'] + '</label><br>\n'
node_info_str = \
translate['Show numbers of accounts within instance metadata']
if get_config_param(base_dir, "showNodeInfoAccounts"):
instance_str += \
edit_check_box(node_info_str, 'showNodeInfoAccounts', True)
else:
instance_str += \
edit_check_box(node_info_str, 'showNodeInfoAccounts', False)
node_info_str = \
translate['Show version number within instance metadata']
if get_config_param(base_dir, "showNodeInfoVersion"):
instance_str += \
edit_check_box(node_info_str, 'showNodeInfoVersion', True)
else:
instance_str += \
edit_check_box(node_info_str, 'showNodeInfoVersion', False)
if get_config_param(base_dir, "verifyAllSignatures"):
instance_str += \
edit_check_box(translate['Verify all signatures'],
'verifyallsignatures', True)
else:
instance_str += \
edit_check_box(translate['Verify all signatures'],
'verifyallsignatures', False)
instance_str += translate['Enabling broch mode'] + '<br>\n'
if get_config_param(base_dir, "brochMode"):
instance_str += \
edit_check_box(translate['Broch mode'], 'brochMode', True)
else:
instance_str += \
edit_check_box(translate['Broch mode'], 'brochMode', False)
# Instance type
instance_str += \
' <br><label class="labels">' + \
translate['Type of instance'] + '</label><br>\n'
instance_str += \
edit_check_box(translate['This is a media instance'],
'mediaInstance', media_instance_str)
instance_str += \
edit_check_box(translate['This is a blogging instance'],
'blogsInstance', blogs_instance_str)
instance_str += \
edit_check_box(translate['This is a news instance'],
'newsInstance', news_instance_str)
instance_str += end_edit_section()
# Role assignments section
role_assign_str = \
begin_edit_section(translate['Role Assignment']) + \
' <b><label class="labels">'
# site moderators
moderators = ''
moderators_file = base_dir + '/accounts/moderators.txt'
if os.path.isfile(moderators_file):
with open(moderators_file, 'r', encoding='utf-8') as mod_file:
moderators = mod_file.read()
subtitle = translate['A list of moderator nicknames. One per line.']
role_assign_str += \
edit_text_area('<b>' + translate['Moderators'] + '</b>', subtitle,
'moderators', moderators, 200, '', False)
# site editors
editors = ''
editors_file = base_dir + '/accounts/editors.txt'
if os.path.isfile(editors_file):
with open(editors_file, 'r', encoding='utf-8') as edit_file:
editors = edit_file.read()
subtitle = translate['A list of editor nicknames. One per line.']
role_assign_str += \
edit_text_area('<b>' + translate['Site Editors'] + '</b>',
subtitle, 'editors', editors, 200, '', False)
# counselors
counselors = ''
counselors_file = base_dir + '/accounts/counselors.txt'
if os.path.isfile(counselors_file):
with open(counselors_file, 'r', encoding='utf-8') as co_file:
counselors = co_file.read()
role_assign_str += \
edit_text_area('<b>' + translate['Counselors'] + '</b>', None,
'counselors', counselors, 200, '', False)
# artists
artists = ''
artists_file = base_dir + '/accounts/artists.txt'
if os.path.isfile(artists_file):
with open(artists_file, 'r', encoding='utf-8') as art_file:
artists = art_file.read()
role_assign_str += \
edit_text_area('<b>' + translate['Artists'] + '</b>', None,
'artists', artists, 200, '', False)
# site devops
devops = ''
devops_file = base_dir + '/accounts/devops.txt'
if os.path.isfile(devops_file):
with open(devops_file, 'r', encoding='utf-8') as edit_file:
devops = edit_file.read()
subtitle = translate['A list of devops nicknames. One per line.']
role_assign_str += \
edit_text_area('<b>' + translate['Site DevOps'] + '</b>',
subtitle, 'devopslist', devops, 200, '', False)
role_assign_str += end_edit_section()
# Video section
peertube_str = begin_edit_section(translate['Video Settings'])
peertube_instances_str = ''
for url in peertube_instances:
peertube_instances_str += url + '\n'
peertube_str += \
edit_text_area(translate['Peertube Instances'], None,
'ptInstances', peertube_instances_str, 200, '', False)
peertube_str += \
' <br>\n'
yt_replace_domain = get_config_param(base_dir, "youtubedomain")
if not yt_replace_domain:
yt_replace_domain = ''
peertube_str += \
edit_text_field(translate['YouTube Replacement Domain'],
'ytdomain', yt_replace_domain)
peertube_str += end_edit_section()
libretranslate_url = get_config_param(base_dir, 'libretranslateUrl')
libretranslate_api_key = get_config_param(base_dir, 'libretranslateApiKey')
libretranslate_str = \
_html_edit_profile_libre_translate(libretranslate_url,
libretranslate_api_key)
return instance_str, role_assign_str, peertube_str, libretranslate_str
def _html_edit_profile_danger_zone(translate: {}) -> str:
"""danger zone section of Edit Profile screen
"""
edit_profile_form = begin_edit_section(translate['Danger Zone'])
edit_profile_form += \
' <b><label class="labels">' + \
translate['Danger Zone'] + '</label></b><br>\n'
edit_profile_form += \
edit_check_box(translate['Deactivate this account'],
'deactivateThisAccount', False)
edit_profile_form += end_edit_section()
return edit_profile_form
def _html_system_monitor(nickname: str, translate: {}) -> str:
"""Links to performance graphs
"""
system_monitor_str = begin_edit_section(translate['System Monitor'])
system_monitor_str += '<p><a href="/users/' + nickname + \
'/performance?graph=get">📊 GET</a></p>'
system_monitor_str += '<p><a href="/users/' + nickname + \
'/performance?graph=inbox">📊 INBOX</a></p>'
system_monitor_str += '<p><a href="/users/' + nickname + \
'/performance?graph=post">📊 POST</a></p>'
system_monitor_str += end_edit_section()
return system_monitor_str
def _html_edit_profile_skills(base_dir: str, nickname: str, domain: str,
translate: {}) -> str:
"""skills section of Edit Profile screen
"""
system_language = 'en'
skills = get_skills(base_dir, nickname, domain)
skills_str = ''
skill_ctr = 1
if skills:
for skill_desc, skill_value in skills.items():
if is_filtered(base_dir, nickname, domain, skill_desc,
system_language):
continue
skills_str += \
'<p><input type="text" placeholder="' + translate['Skill'] + \
' ' + str(skill_ctr) + '" name="skillName' + str(skill_ctr) + \
'" value="' + skill_desc + '" style="width:40%">' + \
'<input type="range" min="1" max="100" ' + \
'class="slider" name="skillValue' + \
str(skill_ctr) + '" value="' + str(skill_value) + '"></p>'
skill_ctr += 1
skills_str += \
'<p><input type="text" placeholder="Skill ' + str(skill_ctr) + \
'" name="skillName' + str(skill_ctr) + \
'" value="" style="width:40%">' + \
'<input type="range" min="1" max="100" ' + \
'class="slider" name="skillValue' + \
str(skill_ctr) + '" value="50"></p>' + end_edit_section()
idx = 'If you want to participate within organizations then you ' + \
'can indicate some skills that you have and approximate ' + \
'proficiency levels. This helps organizers to construct ' + \
'teams with an appropriate combination of skills.'
edit_profile_form = \
begin_edit_section(translate['Skills']) + \
' <b><label class="labels">' + \
translate['Skills'] + '</label></b><br>\n' + \
' <label class="labels">' + \
translate[idx] + '</label>\n' + skills_str
return edit_profile_form
def _html_edit_profile_git_projects(base_dir: str, nickname: str, domain: str,
translate: {}) -> str:
"""git projects section of edit profile screen
"""
git_projects_str = ''
git_projects_filename = \
acct_dir(base_dir, nickname, domain) + '/gitprojects.txt'
if os.path.isfile(git_projects_filename):
with open(git_projects_filename, 'r', encoding='utf-8') as git_file:
git_projects_str = git_file.read()
edit_profile_form = begin_edit_section(translate['Git Projects'])
idx = 'List of project names that you wish to receive git patches for'
edit_profile_form += \
edit_text_area(translate[idx], None, 'gitProjects', git_projects_str,
100, '', False)
edit_profile_form += end_edit_section()
return edit_profile_form
def _html_edit_profile_shared_items(base_dir: str, translate: {}) -> str:
"""shared items section of edit profile screen
"""
shared_items_str = ''
shared_items_federated_domains_str = \
get_config_param(base_dir, 'sharedItemsFederatedDomains')
if shared_items_federated_domains_str:
shared_items_federated_domains_list = \
shared_items_federated_domains_str.split(',')
for shared_federated_domain in shared_items_federated_domains_list:
shared_items_str += shared_federated_domain.strip() + '\n'
edit_profile_form = begin_edit_section(translate['Shares'])
idx = 'List of domains which can access the shared items catalog'
edit_profile_form += \
edit_text_area(translate[idx], None, 'shareDomainList',
shared_items_str, 200, '', False)
edit_profile_form += end_edit_section()
return edit_profile_form
def _html_edit_profile_filtering(base_dir: str, nickname: str, domain: str,
user_agents_blocked: str,
crawlers_allowed: str,
translate: {}, reply_interval_hours: int,
cw_lists: {}, lists_enabled: str,
buy_sites: {}) -> str:
"""Filtering and blocking section of edit profile screen
"""
filter_str = ''
filter_filename = \
acct_dir(base_dir, nickname, domain) + '/filters.txt'
if os.path.isfile(filter_filename):
with open(filter_filename, 'r', encoding='utf-8') as filterfile:
filter_str = filterfile.read()
filter_bio_str = ''
filter_bio_filename = \
acct_dir(base_dir, nickname, domain) + '/filters_bio.txt'
if os.path.isfile(filter_bio_filename):
with open(filter_bio_filename, 'r', encoding='utf-8') as filterfile:
filter_bio_str = filterfile.read()
switch_str = ''
switch_filename = \
acct_dir(base_dir, nickname, domain) + '/replacewords.txt'
if os.path.isfile(switch_filename):
with open(switch_filename, 'r', encoding='utf-8') as switchfile:
switch_str = switchfile.read()
auto_tags = ''
auto_tags_filename = \
acct_dir(base_dir, nickname, domain) + '/autotags.txt'
if os.path.isfile(auto_tags_filename):
with open(auto_tags_filename, 'r', encoding='utf-8') as auto_file:
auto_tags = auto_file.read()
auto_cw = ''
auto_cw_filename = \
acct_dir(base_dir, nickname, domain) + '/autocw.txt'
if os.path.isfile(auto_cw_filename):
with open(auto_cw_filename, 'r', encoding='utf-8') as cw_file:
auto_cw = cw_file.read()
blocked_str = get_account_blocks(base_dir, nickname, domain)
dm_allowed_instances_str = ''
dm_allowed_instances_filename = \
acct_dir(base_dir, nickname, domain) + '/dmAllowedInstances.txt'
if os.path.isfile(dm_allowed_instances_filename):
with open(dm_allowed_instances_filename, 'r',
encoding='utf-8') as dm_file:
dm_allowed_instances_str = dm_file.read()
allowed_instances_str = ''
allowed_instances_filename = \
acct_dir(base_dir, nickname, domain) + '/allowedinstances.txt'
if os.path.isfile(allowed_instances_filename):
with open(allowed_instances_filename, 'r',
encoding='utf-8') as allow_file:
allowed_instances_str = allow_file.read()
edit_profile_form = begin_edit_section(translate['Filtering and Blocking'])
idx = 'Hours after posting during which replies are allowed'
edit_profile_form += \
' <label class="labels">' + \
translate[idx] + \
':</label> <input type="number" name="replyhours" ' + \
'min="0" max="999999999999" step="1" ' + \
'value="' + str(reply_interval_hours) + '"><br>\n'
edit_profile_form += \
'<label class="labels">' + \
translate['City for spoofed GPS image metadata'] + \
'</label><br>\n'
city = ''
city_filename = acct_dir(base_dir, nickname, domain) + '/city.txt'
if os.path.isfile(city_filename):
with open(city_filename, 'r', encoding='utf-8') as city_file:
city1 = city_file.read()
city = remove_eol(city1)
locations_filename = base_dir + '/custom_locations.txt'
if not os.path.isfile(locations_filename):
locations_filename = base_dir + '/locations.txt'
cities = []
with open(locations_filename, 'r', encoding='utf-8') as loc_file:
cities = loc_file.readlines()
cities.sort()
edit_profile_form += ' <select id="cityDropdown" ' + \
'name="cityDropdown" class="theme">\n'
city = city.lower()
for city_name in cities:
if ':' not in city_name:
continue
city_selected = ''
city_name = city_name.split(':')[0]
city_name = city_name.lower()
if city:
if city in city_name:
city_selected = ' selected'
edit_profile_form += \
' <option value="' + city_name + \
'"' + city_selected.title() + '>' + \
city_name + '</option>\n'
edit_profile_form += ' </select><br>\n'
edit_profile_form += \
' <b><label class="labels">' + \
translate['Filtered words'] + '</label></b>\n' + \
' <br><label class="labels">' + \
translate['One per line'] + '</label>\n' + \
' <textarea id="message" ' + \
'name="filteredWords" style="height:200px" spellcheck="false">' + \
filter_str + '</textarea>\n' + \
' <br><b><label class="labels">' + \
translate['Filtered words within bio'] + '</label></b>\n' + \
' <br><label class="labels">' + \
translate['One per line'] + '</label>\n' + \
' <textarea id="message" ' + \
'name="filteredWordsBio" style="height:200px" spellcheck="false">' + \
filter_bio_str + '</textarea>\n' + \
' <br><b><label class="labels">' + \
translate['Word Replacements'] + '</label></b>\n' + \
' <br><label class="labels">A -> B</label>\n' + \
' <textarea id="message" name="switchwords" ' + \
'style="height:200px" spellcheck="false">' + \
switch_str + '</textarea>\n' + \
' <br><b><label class="labels">' + \
translate['Autogenerated Hashtags'] + '</label></b>\n' + \
' <br><label class="labels">A -> #B</label>\n' + \
' <textarea id="message" name="autoTags" ' + \
'style="height:200px" spellcheck="false">' + \
auto_tags + '</textarea>\n' + \
' <br><b><label class="labels">' + \
translate['Autogenerated Content Warnings'] + '</label></b>\n' + \
' <br><label class="labels">A -> B</label>\n' + \
' <textarea id="message" name="autoCW" ' + \
'style="height:200px" spellcheck="true">' + auto_cw + '</textarea>\n'
idx = 'Blocked accounts, one per line, in the form ' + \
'nickname@domain or *@blockeddomain'
edit_profile_form += \
edit_text_area(translate['Blocked accounts'], None, 'blocked',
blocked_str, 200, '', False)
# import and export blocks
edit_profile_form += \
' <label class="labels">' + \
translate['Import Blocks'] + '</label>\n'
edit_profile_form += ' <input type="file" id="importBlocks" '
edit_profile_form += 'name="importBlocks" '
edit_profile_form += 'accept="' + BLOCKFILE_FORMATS + '">\n'
edit_profile_form += \
' <label class="labels">' + \
translate['Export Blocks'] + '</label><br>\n'
edit_profile_form += \
' <button type="submit" class="button" ' + \
'name="submitExportBlocks">➤</button><br>\n'
idx = 'Direct messages are always allowed from these instances.'
edit_profile_form += \
edit_text_area(translate['Direct Message permitted instances'], None,
'dmAllowedInstances', dm_allowed_instances_str,
200, '', False)
idx = 'Federate only with a defined set of instances. ' + \
'One domain name per line.'
edit_profile_form += \
' <br><b><label class="labels">' + \
translate['Federation list'] + '</label></b>\n' + \
' <br><label class="labels">' + \
translate[idx] + '</label>\n' + \
' <textarea id="message" name="allowedInstances" ' + \
'style="height:200px" spellcheck="false">' + \
allowed_instances_str + '</textarea>\n'
if is_moderator(base_dir, nickname):
edit_profile_form += \
'<a href="/users/' + nickname + '/crawlers">' + \
translate['Known Web Crawlers'] + '</a><br>\n'
user_agents_blocked_str = ''
for uagent in user_agents_blocked:
if user_agents_blocked_str:
user_agents_blocked_str += '\n'
user_agents_blocked_str += uagent
edit_profile_form += \
edit_text_area(translate['Blocked User Agents'], None,
'userAgentsBlockedStr', user_agents_blocked_str,
200, '', False)
edit_profile_form += \
'<a href="/users/' + nickname + '/bots.txt">' + \
translate['Known Search Bots'] + '</a><br>\n'
crawlers_allowed_str = ''
for uagent in crawlers_allowed:
if crawlers_allowed_str:
crawlers_allowed_str += '\n'
crawlers_allowed_str += uagent
edit_profile_form += \
edit_text_area(translate['Web Bots Allowed'], None,
'crawlersAllowedStr', crawlers_allowed_str,
200, '', False)
buy_domains_list_str = ''
for buy_icon_text, buy_url in buy_sites.items():
if buy_icon_text != buy_url:
buy_domains_list_str += \
buy_icon_text + ' ' + buy_url.strip() + '\n'
else:
buy_domains_list_str += buy_url.strip() + '\n'
buy_domains_str = \
"Buy links are allowed from the following domains"
edit_profile_form += \
edit_text_area(translate[buy_domains_str], None,
'buySitesStr', buy_domains_list_str,
200, '', False)
cw_lists_str = ''
for name, _ in cw_lists.items():
variablename = get_cw_list_variable(name)
list_is_enabled = False
if lists_enabled:
if name in lists_enabled:
list_is_enabled = True
if translate.get(name):
name = translate[name]
cw_lists_str += \
edit_check_box(name, variablename, list_is_enabled)
if cw_lists_str:
idx = 'Add content warnings for the following sites'
edit_profile_form += \
'<label class="labels">' + translate[idx] + ':</label>\n' + \
'<br>' + cw_lists_str
edit_profile_form += end_edit_section()
return edit_profile_form
def _html_edit_profile_change_password(translate: {}) -> str:
"""Change password section of edit profile screen
"""
edit_profile_form = \
begin_edit_section(translate['Change Password']) + \
'<label class="labels">' + translate['Change Password'] + \
'</label><br>\n' + \
' <input type="password" name="password" ' + \
'value=""><br>\n' + \
'<label class="labels">' + translate['Confirm Password'] + \
'</label><br>\n' + \
' <input type="password" name="passwordconfirm" value="">\n' + \
end_edit_section()
return edit_profile_form
def _html_edit_profile_libre_translate(libretranslate_url: str,
libretranslate_api_key: str) -> str:
"""Change automatic translation settings
"""
edit_profile_form = begin_edit_section('LibreTranslate')
edit_profile_form += \
edit_text_field('URL', 'libretranslateUrl', libretranslate_url,
'http://0.0.0.0:5000')
edit_profile_form += \
edit_text_field('API Key', 'libretranslateApiKey',
libretranslate_api_key)
edit_profile_form += end_edit_section()
return edit_profile_form
def _html_edit_profile_background(news_instance: bool, translate: {}) -> str:
"""Background images section of edit profile screen
"""
idx = 'The files attached below should be no larger than ' + \
'10MB in total uploaded at once.'
edit_profile_form = \
begin_edit_section(translate['Background Images']) + \
' <label class="labels">' + translate[idx] + '</label><br><br>\n'
if not news_instance:
image_formats = get_image_formats()
edit_profile_form += \
' <label class="labels">' + \
translate['Background image'] + '</label>\n' + \
' <input type="file" id="image" name="image"' + \
' accept="' + image_formats + '">\n' + \
' <br><label class="labels">' + \
translate['Timeline banner image'] + '</label>\n' + \
' <input type="file" id="banner" name="banner"' + \
' accept="' + image_formats + '">\n' + \
' <br><label class="labels">' + \
translate['Search banner image'] + '</label>\n' + \
' <input type="file" id="search_banner" ' + \
'name="search_banner"' + \
' accept="' + image_formats + '">\n' + \
' <br><label class="labels">' + \
translate['Left column image'] + '</label>\n' + \
' <input type="file" id="left_col_image" ' + \
'name="left_col_image"' + \
' accept="' + image_formats + '">\n' + \
' <br><label class="labels">' + \
translate['Right column image'] + '</label>\n' + \
' <input type="file" id="right_col_image" ' + \
'name="right_col_image"' + \
' accept="' + image_formats + '">\n'
edit_profile_form += end_edit_section()
return edit_profile_form
def _html_edit_profile_contact_info(email_address: str,
xmpp_address: str,
matrix_address: str,
ssb_address: str,
tox_address: str,
briar_address: str,
cwtch_address: str,
translate: {}) -> str:
"""Contact Information section of edit profile screen
"""
edit_profile_form = begin_edit_section(translate['Contact Details'])
edit_profile_form += edit_text_field(translate['Email'],
'email', email_address)
edit_profile_form += edit_text_field(translate['XMPP'],
'xmppAddress', xmpp_address)
edit_profile_form += edit_text_field(translate['Matrix'],
'matrixAddress', matrix_address)
edit_profile_form += edit_text_field('SSB', 'ssbAddress', ssb_address)
edit_profile_form += edit_text_field('Tox', 'toxAddress', tox_address)
edit_profile_form += edit_text_field('Briar', 'briarAddress',
briar_address)
edit_profile_form += edit_text_field('Cwtch', 'cwtchAddress',
cwtch_address)
edit_profile_form += end_edit_section()
return edit_profile_form
def _html_edit_notifications(base_dir: str, nickname: str, domain: str,
translate: {}) -> str:
"""Notifications settings
"""
ntfy_url = "ntfy.sh"
ntfy_topic = ''
ntfy_url_file = \
acct_dir(base_dir, nickname, domain) + '/.ntfy_url'
ntfy_topic_file = \
acct_dir(base_dir, nickname, domain) + '/.ntfy_topic'
if os.path.isfile(ntfy_url_file):
try:
with open(ntfy_url_file, 'r', encoding='utf-8') as fp_ntfy:
ntfy_url = fp_ntfy.read()
except OSError:
print('EX: unable to read ' + ntfy_url_file)
if os.path.isfile(ntfy_topic_file):
try:
with open(ntfy_topic_file, 'r', encoding='utf-8') as fp_ntfy:
ntfy_topic = fp_ntfy.read()
except OSError:
print('EX: unable to read ' + ntfy_topic_file)
edit_profile_form = begin_edit_section(translate['Notifications'])
edit_profile_form += edit_text_field(translate['ntfy URL'],
'ntfyUrl', ntfy_url)
edit_profile_form += edit_text_field(translate['ntfy topic'],
'ntfyTopic', ntfy_topic)
edit_profile_form += end_edit_section()
return edit_profile_form
def _html_edit_profile_import_export(nickname: str, domain: str,
translate: {}) -> str:
"""Contact Information section of edit profile screen
"""
edit_profile_form = begin_edit_section(translate['Import and Export'])
edit_profile_form += \
'<p><label class="labels">' + \
translate['Import Follows'] + '</label>\n'
edit_profile_form += '<input type="file" id="importFollows" '
edit_profile_form += 'name="importFollows" '
edit_profile_form += 'accept=".csv"></p>\n'
edit_profile_form += \
'<p><a href="/users/' + nickname + \
'/followingaccounts"><label class="labels">' + \
translate['Following'] + '</label></a>'
edit_profile_form += \
' <a href="/users/' + nickname + '/followingaccounts.csv" ' + \
'download="' + nickname + '@' + domain + '_following.csv">' + \
'<label class="labels">⇩ CSV</label></a></p>\n'
edit_profile_form += \
'<p><a href="/users/' + nickname + \
'/followersaccounts"><label class="labels">' + \
translate['Followers'] + '</label></a><br></p>\n'
edit_profile_form += end_edit_section()
return edit_profile_form
def _html_edit_profile_encryption_keys(pgp_fingerprint: str,
pgp_pub_key: str,
enigma_pub_key: str,
translate: {}) -> str:
"""Contact Information section of edit profile screen
"""
edit_profile_form = begin_edit_section(translate['Encryption Keys'])
enigma_url = 'https://github.com/enigma-reloaded/enigma-reloaded'
edit_profile_form += \
edit_text_field('<a href="' + enigma_url + '">Enigma</a>',
'enigmapubkey', enigma_pub_key)
edit_profile_form += edit_text_field(translate['PGP Fingerprint'],
'openpgp', pgp_fingerprint)
edit_profile_form += \
edit_text_area(translate['PGP'], None, 'pgp', pgp_pub_key, 600,
'-----BEGIN PGP PUBLIC KEY BLOCK-----', False)
edit_profile_form += end_edit_section()
return edit_profile_form
def _html_edit_profile_options(is_admin: bool,
manually_approves_followers: str,
reject_spam_actors: str,
is_bot: str, is_group: str,
follow_dms: str, remove_twitter: str,
notify_likes: str, notify_reactions: str,
hide_like_button: str,
hide_reaction_button: str,
translate: {}, bold_reading: bool,
nickname: str,
min_images_for_accounts: [],
reverse_sequence: []) -> str:
"""option checkboxes section of edit profile screen
"""
edit_profile_form = ' <div class="container">\n'
edit_profile_form += \
edit_check_box(translate['Approve follower requests'],
'approveFollowers', manually_approves_followers)
edit_profile_form += \
edit_check_box(translate['Reject spam accounts'],
'rejectSpamActors', reject_spam_actors)
edit_profile_form += \
edit_check_box(translate['This is a bot account'],
'isBot', is_bot)
if is_admin:
edit_profile_form += \
edit_check_box(translate['This is a group account'],
'isGroup', is_group)
edit_profile_form += \
edit_check_box(translate['Only people I follow can send me DMs'],
'followDMs', follow_dms)
edit_profile_form += \
edit_check_box(translate['Remove Twitter posts'],
'removeTwitter', remove_twitter)
edit_profile_form += \
edit_check_box(translate['Notify when posts are liked'],
'notifyLikes', notify_likes)
edit_profile_form += \
edit_check_box(translate['Notify on emoji reactions'],
'notifyReactions', notify_reactions)
edit_profile_form += \
edit_check_box(translate["Don't show the Like button"],
'hideLikeButton', hide_like_button)
edit_profile_form += \
edit_check_box(translate["Don't show the Reaction button"],
'hideReactionButton', hide_reaction_button)
bold_str = bold_reading_string(translate['Bold reading'])
edit_profile_form += \
edit_check_box(bold_str, 'boldReading', bold_reading)
minimize_all_images = False
if nickname in min_images_for_accounts:
minimize_all_images = True
minimize_all_images_str = \
bold_reading_string(translate['Minimize all images'])
edit_profile_form += \
edit_check_box(minimize_all_images_str, 'minimizeAllImages',
minimize_all_images)
reverse = False
if nickname in reverse_sequence:
reverse = True
reverse_str = \
bold_reading_string(translate['Reverse timelines'])
edit_profile_form += \
edit_check_box(reverse_str, 'reverseTimelines', reverse)
edit_profile_form += ' </div>\n'
return edit_profile_form
def _get_supported_languagesSorted(base_dir: str) -> str:
"""Returns a list of supported languages
"""
lang_list = get_supported_languages(base_dir)
if not lang_list:
return ''
lang_list.sort()
languages_str = ''
for lang in lang_list:
if languages_str:
languages_str += ' / ' + lang
else:
languages_str = lang
return languages_str
def _html_edit_profile_main(base_dir: str, display_nickname: str, bio_str: str,
moved_to: str, donate_url: str, website_url: str,
gemini_link: str, blog_address: str,
actor_json: {}, translate: {},
nickname: str, domain: str,
max_recent_posts: int) -> str:
"""main info on edit profile screen
"""
image_formats = get_image_formats()
edit_profile_form = ' <div class="container">\n'
edit_profile_form += \
edit_text_field(translate['Nickname'], 'displayNickname',
display_nickname)
edit_profile_form += \
edit_text_area(translate['Your bio'], None, 'bio', bio_str,
200, '', True)
edit_profile_form += \
' <label class="labels">' + translate['Avatar image'] + \
'</label>\n' + \
' <input type="file" id="avatar" name="avatar"' + \
' accept="' + image_formats + '">\n'
occupation_name = ''
if actor_json.get('hasOccupation'):
occupation_name = get_occupation_name(actor_json)
edit_profile_form += \
edit_text_field(translate['Occupation'], 'occupationName',
occupation_name)
also_known_as_str = ''
if actor_json.get('alsoKnownAs'):
also_known_as = actor_json['alsoKnownAs']
ctr = 0
for alt_actor in also_known_as:
if ctr > 0:
also_known_as_str += ', '
ctr += 1
also_known_as_str += alt_actor
edit_profile_form += \
edit_text_field(translate['Other accounts'], 'alsoKnownAs',
also_known_as_str, 'https://...')
edit_profile_form += \
edit_text_field(translate['Moved to new account address'], 'movedTo',
moved_to, 'https://...')
edit_profile_form += \
edit_text_field(translate['Donations link'], 'donateUrl',
donate_url, 'https://...')
edit_profile_form += \
edit_text_field(translate['Website'], 'websiteUrl',
website_url, 'https://...')
edit_profile_form += \
edit_text_field('Gemini', 'geminiLink',
gemini_link, 'gemini://...')
edit_profile_form += \
edit_text_field('Blog', 'blogAddress', blog_address, 'https://...')
languages_list_str = _get_supported_languagesSorted(base_dir)
show_languages = get_actor_languages(actor_json)
edit_profile_form += \
edit_text_field(translate['Languages'], 'showLanguages',
show_languages, languages_list_str)
timezone = get_account_timezone(base_dir, nickname, domain)
edit_profile_form += \
edit_text_field(translate['Time Zone'], 'timeZone',
timezone, 'Europe/London')
post_expiry_period_days = \
get_post_expiry_days(base_dir, nickname, domain)
edit_profile_form += \
edit_number_field(translate['Post expiry period in days'],
'postExpiryPeriod', post_expiry_period_days,
0, 9999999999999999999999, 0)
keep_dms = get_post_expiry_keep_dms(base_dir, nickname, domain)
edit_profile_form += '<br>\n' + \
edit_check_box(translate['Keep DMs during post expiry'],
'expiryKeepDMs', keep_dms)
max_profile_posts = \
get_max_profile_posts(base_dir, nickname, domain, max_recent_posts)
edit_profile_form += \
edit_number_field(translate['Preview posts on profile screen'],
'maxRecentProfilePosts', max_profile_posts,
1, 20, max_recent_posts)
edit_profile_form += ' </div>\n'
return edit_profile_form
def _html_edit_profile_top_banner(base_dir: str,
nickname: str, domain: str, domain_full: str,
default_timeline: str, banner_file: str,
path: str, access_keys: {},
translate: {}) -> str:
"""top banner on edit profile screen
"""
edit_profile_form = \
'<a href="/users/' + nickname + '/' + default_timeline + '">' + \
'<img loading="lazy" decoding="async" ' + \
'class="timeline-banner" src="' + \
'/users/' + nickname + '/' + banner_file + '" alt="" /></a>\n'
edit_profile_form += \
'<form enctype="multipart/form-data" method="POST" ' + \
'accept-charset="UTF-8" action="' + path + '/profiledata">\n'
edit_profile_form += ' <div class="vertical-center">\n'
edit_profile_form += \
' <h1>' + translate['Profile for'] + \
' ' + nickname + '@' + domain_full + '</h1>'
edit_profile_form += ' <div class="container">\n'
edit_profile_form += \
' <center>\n' + \
' <input type="submit" name="submitProfile" ' + \
'accesskey="' + access_keys['submitButton'] + '" ' + \
'value="' + translate['Publish'] + '">\n' + \
' </center>\n'
edit_profile_form += ' </div>\n'
if scheduled_posts_exist(base_dir, nickname, domain):
edit_profile_form += ' <div class="container">\n'
edit_profile_form += \
edit_check_box(translate['Remove scheduled posts'],
'removeScheduledPosts', False)
edit_profile_form += ' </div>\n'
return edit_profile_form
def html_edit_profile(server, translate: {},
base_dir: str, path: str,
domain: str, port: int,
default_timeline: str, theme: str,
peertube_instances: [],
text_mode_banner: str,
user_agents_blocked: [],
crawlers_allowed: [],
access_keys: {},
default_reply_interval_hrs: int,
cw_lists: {}, lists_enabled: str,
system_language: str,
min_images_for_accounts: [],
max_recent_posts: int,
reverse_sequence: [],
buy_sites: {}) -> str:
"""Shows the edit profile screen
"""
path = path.replace('/inbox', '').replace('/outbox', '')
path = path.replace('/shares', '').replace('/wanted', '')
nickname = get_nickname_from_actor(path)
if not nickname:
return ''
domain_full = get_full_domain(domain, port)
bold_reading = False
if server.bold_reading.get(nickname):
bold_reading = True
actor_filename = acct_dir(base_dir, nickname, domain) + '.json'
if not os.path.isfile(actor_filename):
return ''
# filename of the banner shown at the top
banner_file, _ = \
get_profile_background_file(base_dir, nickname, domain, theme)
display_nickname = nickname
is_bot = is_group = follow_dms = remove_twitter = ''
notify_likes = notify_reactions = ''
hide_like_button = hide_reaction_button = media_instance_str = ''
blogs_instance_str = news_instance_str = moved_to = twitter_str = ''
bio_str = donate_url = website_url = gemini_link = email_address = ''
pgp_pub_key = enigma_pub_key = ''
pgp_fingerprint = xmpp_address = matrix_address = ''
ssb_address = blog_address = tox_address = ''
cwtch_address = briar_address = ''
manually_approves_followers = reject_spam_actors = ''
actor_json = load_json(actor_filename)
if actor_json:
if actor_json.get('movedTo'):
moved_to = actor_json['movedTo']
donate_url = get_donation_url(actor_json)
website_url = get_website(actor_json, translate)
gemini_link = get_gemini_link(actor_json, translate)
xmpp_address = get_xmpp_address(actor_json)
matrix_address = get_matrix_address(actor_json)
ssb_address = get_ssb_address(actor_json)
blog_address = get_blog_address(actor_json)
tox_address = get_tox_address(actor_json)
briar_address = get_briar_address(actor_json)
cwtch_address = get_cwtch_address(actor_json)
email_address = get_email_address(actor_json)
enigma_pub_key = get_enigma_pub_key(actor_json)
pgp_pub_key = get_pgp_pub_key(actor_json)
pgp_fingerprint = get_pgp_fingerprint(actor_json)
if actor_json.get('name'):
if not is_filtered(base_dir, nickname, domain, actor_json['name'],
system_language):
display_nickname = actor_json['name']
if actor_json.get('summary'):
bio_str = \
actor_json['summary'].replace('<p>', '').replace('</p>', '')
if is_filtered(base_dir, nickname, domain, bio_str,
system_language):
bio_str = ''
bio_str = remove_html(bio_str)
if actor_json.get('manuallyApprovesFollowers'):
if actor_json['manuallyApprovesFollowers']:
manually_approves_followers = 'checked'
else:
manually_approves_followers = ''
reject_spam_actors = ''
actor_spam_filter_filename = \
acct_dir(base_dir, nickname, domain) + '/.reject_spam_actors'
if os.path.isfile(actor_spam_filter_filename):
reject_spam_actors = 'checked'
if actor_json.get('type'):
if actor_json['type'] == 'Service':
is_bot = 'checked'
is_group = ''
elif actor_json['type'] == 'Group':
is_group = 'checked'
is_bot = ''
account_dir = acct_dir(base_dir, nickname, domain)
if os.path.isfile(account_dir + '/.followDMs'):
follow_dms = 'checked'
if os.path.isfile(account_dir + '/.removeTwitter'):
remove_twitter = 'checked'
if os.path.isfile(account_dir + '/.notifyLikes'):
notify_likes = 'checked'
if os.path.isfile(account_dir + '/.notifyReactions'):
notify_reactions = 'checked'
if os.path.isfile(account_dir + '/.hideLikeButton'):
hide_like_button = 'checked'
if os.path.isfile(account_dir + '/.hideReactionButton'):
hide_reaction_button = 'checked'
media_instance = get_config_param(base_dir, "mediaInstance")
if media_instance:
if media_instance is True:
media_instance_str = 'checked'
blogs_instance_str = news_instance_str = ''
news_instance = get_config_param(base_dir, "newsInstance")
if news_instance:
if news_instance is True:
news_instance_str = 'checked'
blogs_instance_str = media_instance_str = ''
blogs_instance = get_config_param(base_dir, "blogsInstance")
if blogs_instance:
if blogs_instance is True:
blogs_instance_str = 'checked'
media_instance_str = news_instance_str = ''
css_filename = base_dir + '/epicyon-profile.css'
if os.path.isfile(base_dir + '/epicyon.css'):
css_filename = base_dir + '/epicyon.css'
instance_str = ''
role_assign_str = ''
peertube_str = ''
libretranslate_str = ''
system_monitor_str = ''
graphics_str = ''
shares_federation_str = ''
admin_nickname = get_config_param(base_dir, 'admin')
if is_artist(base_dir, nickname) or \
path.startswith('/users/' + str(admin_nickname) + '/'):
graphics_str = _html_edit_profile_graphic_design(base_dir, translate)
is_admin = False
if admin_nickname:
if path.startswith('/users/' + admin_nickname + '/'):
is_admin = True
twitter_str = \
_html_edit_profile_twitter(base_dir, translate, remove_twitter)
# shared items section
shares_federation_str = \
_html_edit_profile_shared_items(base_dir, translate)
instance_str, role_assign_str, peertube_str, libretranslate_str = \
_html_edit_profile_instance(base_dir, translate,
peertube_instances,
media_instance_str,
blogs_instance_str,
news_instance_str)
if is_admin or is_devops(base_dir, nickname):
system_monitor_str = _html_system_monitor(nickname, translate)
instance_title = get_config_param(base_dir, 'instanceTitle')
edit_profile_form = \
html_header_with_external_style(css_filename, instance_title, None)
# keyboard navigation
user_path_str = '/users/' + nickname
user_timeline_str = '/users/' + nickname + '/' + default_timeline
menu_timeline = \
html_hide_from_screen_reader('🏠') + ' ' + \
translate['Switch to timeline view']
menu_profile = \
html_hide_from_screen_reader('👤') + ' ' + \
translate['Switch to profile view']
nav_links = {
menu_profile: user_path_str,
menu_timeline: user_timeline_str
}
nav_access_keys = {
menu_profile: 'p',
menu_timeline: 't'
}
edit_profile_form += \
html_keyboard_navigation(text_mode_banner, nav_links, nav_access_keys)
# top banner
edit_profile_form += \
_html_edit_profile_top_banner(base_dir, nickname, domain, domain_full,
default_timeline, banner_file,
path, access_keys, translate)
# main info
edit_profile_form += \
_html_edit_profile_main(base_dir, display_nickname, bio_str,
moved_to, donate_url, website_url,
gemini_link,
blog_address, actor_json, translate,
nickname, domain, max_recent_posts)
# Option checkboxes
edit_profile_form += \
_html_edit_profile_options(is_admin, manually_approves_followers,
reject_spam_actors,
is_bot, is_group, follow_dms,
remove_twitter,
notify_likes, notify_reactions,
hide_like_button, hide_reaction_button,
translate, bold_reading,
nickname, min_images_for_accounts,
reverse_sequence)
# Contact information
edit_profile_form += \
_html_edit_profile_contact_info(email_address,
xmpp_address, matrix_address,
ssb_address, tox_address,
briar_address,
cwtch_address, translate)
# notification settings
edit_profile_form += \
_html_edit_notifications(base_dir, nickname, domain, translate)
# Import and export
edit_profile_form += \
_html_edit_profile_import_export(nickname, domain, translate)
# Encryption Keys
edit_profile_form += \
_html_edit_profile_encryption_keys(pgp_fingerprint,
pgp_pub_key, enigma_pub_key,
translate)
# Customize images and banners
edit_profile_form += \
_html_edit_profile_background(news_instance, translate)
# Change password
edit_profile_form += _html_edit_profile_change_password(translate)
# automatic translations
edit_profile_form += libretranslate_str
# system monitor
edit_profile_form += system_monitor_str
# Filtering and blocking section
reply_interval_hours = \
get_reply_interval_hours(base_dir, nickname, domain,
default_reply_interval_hrs)
edit_profile_form += \
_html_edit_profile_filtering(base_dir, nickname, domain,
user_agents_blocked, crawlers_allowed,
translate, reply_interval_hours,
cw_lists, lists_enabled, buy_sites)
# git projects section
edit_profile_form += \
_html_edit_profile_git_projects(base_dir, nickname, domain, translate)
# Skills section
edit_profile_form += \
_html_edit_profile_skills(base_dir, nickname, domain, translate)
edit_profile_form += role_assign_str + peertube_str + graphics_str
edit_profile_form += shares_federation_str + twitter_str + instance_str
# danger zone section
edit_profile_form += _html_edit_profile_danger_zone(translate)
edit_profile_form += ' <div class="container">\n'
edit_profile_form += \
' <center>\n' + \
' <input type="submit" name="submitProfile" value="' + \
translate['Publish'] + '">\n' + \
' </center>\n'
edit_profile_form += ' </div>\n'
edit_profile_form += ' </div>\n'
edit_profile_form += '</form>\n'
edit_profile_form += html_footer()
return edit_profile_form
def _individual_follow_as_html(signing_priv_key_pem: str,
translate: {},
base_dir: str, session,
cached_webfingers: {},
person_cache: {}, domain: str,
follow_url: str,
authorized: bool,
actor_nickname: str,
http_prefix: str,
project_version: str,
dormant: bool,
debug: bool,
buttons=[]) -> str:
"""An individual follow entry on the profile screen
"""
follow_url_nickname = get_nickname_from_actor(follow_url)
if not follow_url_nickname:
return ''
follow_url_domain, follow_url_port = get_domain_from_actor(follow_url)
if not follow_url_domain:
return ''
follow_url_domain_full = \
get_full_domain(follow_url_domain, follow_url_port)
title_str = '@' + follow_url_nickname + '@' + follow_url_domain_full
avatar_url = \
get_person_avatar_url(base_dir, follow_url, person_cache)
if not avatar_url:
avatar_url = follow_url + '/avatar.png'
display_name = get_display_name(base_dir, follow_url, person_cache)
is_group = False
if not display_name:
# lookup the correct webfinger for the follow_url
follow_url_handle = follow_url_nickname + '@' + follow_url_domain_full
follow_url_wf = \
webfinger_handle(session, follow_url_handle, http_prefix,
cached_webfingers,
domain, __version__, debug, False,
signing_priv_key_pem)
origin_domain = domain
(_, _, _, _, _, avatar_url2,
display_name, is_group) = get_person_box(signing_priv_key_pem,
origin_domain,
base_dir, session,
follow_url_wf,
person_cache,
project_version,
http_prefix,
follow_url_nickname,
domain, 'outbox', 43036)
if avatar_url2:
avatar_url = avatar_url2
if display_name:
display_name = \
add_emoji_to_display_name(None, base_dir, http_prefix,
actor_nickname, domain,
display_name, False, translate)
title_str = display_name
if dormant:
title_str += ' 💤'
buttons_str = ''
if authorized:
for btn in buttons:
if btn == 'block':
buttons_str += \
'<a href="/users/' + actor_nickname + \
'?options=' + follow_url + \
';1;' + avatar_url + \
'"><button class="buttonunfollow">' + \
translate['Block'] + '</button></a>\n'
elif btn == 'unfollow':
unfollow_str = 'Unfollow'
if is_group or \
is_group_account(base_dir,
follow_url_nickname, follow_url_domain):
unfollow_str = 'Leave'
buttons_str += \
'<a href="/users/' + actor_nickname + \
'?options=' + follow_url + \
';1;' + avatar_url + \
'"><button class="buttonunfollow">' + \
translate[unfollow_str] + '</button></a>\n'
elif btn == 'moveAccount':
buttons_str += \
'<a href="/users/' + actor_nickname + \
'?options=' + follow_url + \
';1;' + avatar_url + \
'"><button class="buttonunfollow">' + \
translate['Move'] + '</button></a>\n'
result_str = '<div class="container">\n'
result_str += \
'<a href="/users/' + actor_nickname + '?options=' + \
follow_url + ';1;' + avatar_url + '">\n'
result_str += '<p><img loading="lazy" decoding="async" ' + \
'src="' + avatar_url + '" alt=" ">'
result_str += title_str + '</a>' + buttons_str + '</p>\n'
result_str += '</div>\n'
return result_str