__filename__ = "webapp_profile.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.3.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@libreserver.org"
__status__ = "Production"
__module_group__ = "Web Interface"
import os
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'
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
if not is_announced_feed_item:
if has_object_dict(post_json_object):
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: []) -> 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']
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']
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 ' ' in profile_description:
if len(profile_description.split(' ')) > 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(' ', '\n')
avatar_description = avatar_description.replace('
', '')
avatar_description = avatar_description.replace('
', '')
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 += \
'\n' + \
' \n' + \
'
\n'
else:
profile_str += \
'\n' + \
' \n' + \
'
\n'
user_feed = \
parse_user_feed(signing_priv_key_pem,
session, outbox_url, as_header, project_version,
http_prefix, from_domain, debug)
if user_feed:
text_mode_separator = '
'
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:
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)
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 \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 \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) -> 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)
domain, port = get_domain_from_actor(profile_json['id'])
if not domain:
return ""
display_name = \
add_emoji_to_display_name(session, base_dir, http_prefix,
nickname, domain,
profile_json['name'], True, translate)
domain_full = get_full_domain(domain, port)
profile_description = \
add_emoji_to_display_name(session, base_dir, http_prefix,
nickname, domain,
profile_json['summary'], 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 = '\n'
donate_section += '
\n'
if donate_url and not is_system_account(nickname):
donate_section += \
' ' + \
'' + translate['Donate'] + \
'
\n'
if website_url:
if site_is_verified(session, base_dir, http_prefix,
nickname, domain,
website_url, False, debug):
donate_section += \
'
\n'
else:
donate_section += \
'' + translate['Website'] + ': ' + \
'' + \
website_url + '
\n'
if gemini_link:
donate_section += \
'' + 'Gemini' + ': ' + \
gemini_link + '
\n'
if email_address:
donate_section += \
'' + translate['Email'] + ': ' + \
email_address + '
\n'
if blog_address:
if site_is_verified(session, base_dir, http_prefix,
nickname, domain,
blog_address, False, debug):
donate_section += \
'
\n'
else:
donate_section += \
'Blog: ' + \
blog_address + '
\n'
if xmpp_address:
donate_section += \
'' + translate['XMPP'] + ': ' + xmpp_address + '
\n'
if matrix_address:
donate_section += \
'' + translate['Matrix'] + ': ' + matrix_address + '
\n'
if ssb_address:
donate_section += \
'SSB: ' + \
ssb_address + '
\n'
if tox_address:
donate_section += \
'Tox: ' + \
tox_address + '
\n'
if briar_address:
if briar_address.startswith('briar://'):
donate_section += \
'' + \
briar_address + '
\n'
else:
donate_section += \
'briar://' + \
briar_address + '
\n'
if cwtch_address:
donate_section += \
'Cwtch: ' + \
cwtch_address + '
\n'
if enigma_pub_key:
donate_section += \
'Enigma: ' + \
enigma_pub_key + '
\n'
if pgp_fingerprint:
donate_section += \
'PGP: ' + \
pgp_fingerprint.replace('\n', ' ') + '
\n'
if pgp_pub_key:
donate_section += \
'' + \
pgp_pub_key.replace('\n', ' ') + '
\n'
donate_section += ' \n'
donate_section += '
\n'
if authorized:
edit_profile_str = \
'' + \
' \n'
logout_str = \
'' + \
' \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 = ' ✨'
base_path = '/users/' + nickname
follow_approvals_section += \
''
profile_description_short = profile_description
if '\n' in profile_description:
if len(profile_description.split('\n')) > 2:
profile_description_short = ''
else:
if ' ' in profile_description:
if len(profile_description.split(' ')) > 2:
profile_description_short = ''
profile_description = profile_description.replace(' ', '\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(' ', '\n')
avatar_description = avatar_description.replace('', '')
avatar_description = avatar_description.replace('
', '')
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 += ''
# search for following or followers
if authorized:
if selected in ('following', 'followers'):
follow_search_str = '\n'
follow_search_str += \
'\n
\n'
profile_str += follow_search_str
# start of #timeline
profile_str += '\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 = \
'
' + \
' '
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) + 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 += '
\n \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) -> 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)
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 += \
' \n' + \
' \n' + \
' \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 += \
' \n' + \
' \n' + \
' \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 += ' '
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 += \
'\n
\n'
for role in roles_list:
if translate.get(role):
profile_str += '
' + translate[role] + ' \n'
else:
profile_str += '' + role + ' \n'
profile_str += ' \n'
if len(profile_str) == 0:
profile_str += \
'@' + nickname + '@' + domain + ' has no roles assigned
\n'
else:
profile_str = '' + profile_str + '
\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 += \
'\n \n'
if len(profile_str) > 0:
profile_str = '' + \
profile_str + '
\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 = '' + profile_str + '
\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 = ' ' + \
translate['Theme'] + ' \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 += ' '
for theme_name in themes:
translated_theme_name = theme_name
if translate.get(theme_name):
translated_theme_name = translate[theme_name]
themes_dropdown += ' ' + \
translated_theme_name + ' '
themes_dropdown += ' '
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('',
' ')
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 += \
' ' + \
translate['Import Theme'] + ' \n'
graphics_str += ' \n'
graphics_str += \
' ' + \
translate['Export Theme'] + ' \n'
graphics_str += \
' ➤ \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 += ' \n'
instance_str += \
edit_text_field(translate['Instance Short Description'],
'instanceDescriptionShort', instance_description_short)
instance_str += ' \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 += ' \n'
instance_str += \
edit_text_field(translate['Custom post submit button text'],
'customSubmitText', custom_submit_text)
instance_str += ' \n'
instance_str += \
' ' + \
translate['Instance Logo'] + ' ' + \
' \n \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 += ' \n'
instance_str += \
' \n' + \
translate['Security'] + ' \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'] + ' \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 += \
' ' + \
translate['Type of instance'] + ' \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']) + \
' '
# 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('' + translate['Moderators'] + ' ', 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('' + translate['Site Editors'] + ' ',
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('' + translate['Counselors'] + ' ', 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('' + translate['Artists'] + ' ', 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('' + translate['Site DevOps'] + ' ',
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 += \
' \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 += \
' ' + \
translate['Danger Zone'] + ' \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 += '📊 GET
'
system_monitor_str += '📊 INBOX
'
system_monitor_str += '📊 POST
'
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 += \
' ' + \
'
'
skill_ctr += 1
skills_str += \
' ' + \
'
' + 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']) + \
' ' + \
translate['Skills'] + ' \n' + \
' ' + \
translate[idx] + ' \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) -> 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 += \
' ' + \
translate[idx] + \
': \n'
edit_profile_form += \
'' + \
translate['City for spoofed GPS image metadata'] + \
' \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 += ' \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 += \
' ' + \
city_name + ' \n'
edit_profile_form += ' \n'
edit_profile_form += \
' ' + \
translate['Filtered words'] + ' \n' + \
' ' + \
translate['One per line'] + ' \n' + \
' \n' + \
' ' + \
translate['Filtered words within bio'] + ' \n' + \
' ' + \
translate['One per line'] + ' \n' + \
' \n' + \
' ' + \
translate['Word Replacements'] + ' \n' + \
' A -> B \n' + \
' \n' + \
' ' + \
translate['Autogenerated Hashtags'] + ' \n' + \
' A -> #B \n' + \
' \n' + \
' ' + \
translate['Autogenerated Content Warnings'] + ' \n' + \
' A -> B \n' + \
' \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)
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 += \
' ' + \
translate['Federation list'] + ' \n' + \
' ' + \
translate[idx] + ' \n' + \
' \n'
if is_moderator(base_dir, nickname):
edit_profile_form += \
'' + \
translate['Known Web Crawlers'] + ' \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 += \
'' + \
translate['Known Search Bots'] + ' \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)
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 += \
'' + translate[idx] + ': \n' + \
' ' + 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']) + \
'' + translate['Change Password'] + \
' \n' + \
' \n' + \
'' + translate['Confirm Password'] + \
' \n' + \
' \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']) + \
' ' + translate[idx] + ' \n'
if not news_instance:
image_formats = get_image_formats()
edit_profile_form += \
' ' + \
translate['Background image'] + ' \n' + \
' \n' + \
' ' + \
translate['Timeline banner image'] + ' \n' + \
' \n' + \
' ' + \
translate['Search banner image'] + ' \n' + \
' \n' + \
' ' + \
translate['Left column image'] + ' \n' + \
' \n' + \
' ' + \
translate['Right column image'] + ' \n' + \
' \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 += \
'' + \
translate['Import Follows'] + ' \n'
edit_profile_form += '
\n'
edit_profile_form += \
'' + \
translate['Following'] + ' '
edit_profile_form += \
' ' + \
'⇩ CSV
\n'
edit_profile_form += \
'' + \
translate['Followers'] + '
\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('Enigma ',
'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 = ' \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 += '
\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 = ' \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 += \
' ' + translate['Avatar image'] + \
' \n' + \
' \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 += ' \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 += '
\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 = \
'' + \
' \n'
edit_profile_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)
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 += \
'' + \
translate['Block'] + ' \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 += \
'' + \
translate[unfollow_str] + ' \n'
elif btn == 'moveAccount':
buttons_str += \
'' + \
translate['Move'] + ' \n'
result_str = '\n'
return result_str