epicyon/webapp_profile.py

2437 lines
100 KiB
Python

__filename__ = "webapp_profile.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.2.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@libreserver.org"
__status__ = "Production"
__module_group__ = "Web Interface"
import os
from pprint import pprint
from webfinger import webfinger_handle
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 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_person_box
from posts import is_moderator
from posts import parse_user_feed
from posts import is_create_inside_announce
from donate import get_donation_url
from donate import get_website
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 jami import get_jami_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 webapp_frontscreen import html_front_screen
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_banner_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 blocking import get_cw_list_variable
def _valid_profile_preview_post(post_json_object: {},
personUrl: str) -> (bool, {}):
"""Returns true if the given post should appear on a person/group profile
after searching for a handle
"""
isAnnouncedFeedItem = False
if is_create_inside_announce(post_json_object):
isAnnouncedFeedItem = 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 = []
if post_json_object.get('cc'):
cc = post_json_object['cc']
newPostJsonObject = {
'object': post_json_object,
'to': post_json_object['to'],
'cc': cc,
'id': post_json_object['id'],
'actor': personUrl,
'type': 'Create'
}
post_json_object = newPostJsonObject
if not post_json_object.get('actor'):
return False, None
if not isAnnouncedFeedItem:
if post_json_object['actor'] != personUrl and \
post_json_object['object']['type'] != 'Page':
return False, None
return True, post_json_object
def html_profile_after_search(css_cache: {},
recent_posts_cache: {}, max_recent_posts: int,
translate: {},
base_dir: str, path: str, http_prefix: str,
nickname: str, domain: str, port: int,
profileHandle: str,
session, cached_webfingers: {}, person_cache: {},
debug: bool, project_version: str,
yt_replace_domain: str,
twitter_replacement_domain: str,
show_published_date_only: bool,
defaultTimeline: str,
peertube_instances: [],
allow_local_network_access: bool,
theme_name: str,
accessKeys: {},
system_language: str,
max_like_count: int,
signing_priv_key_pem: str,
cw_lists: {}, lists_enabled: str) -> str:
"""Show a profile page after a search for a fediverse address
"""
http = False
gnunet = False
if http_prefix == 'http':
http = True
elif http_prefix == 'gnunet':
gnunet = True
profile_json, asHeader = \
get_actor_json(domain, profileHandle, http, gnunet, debug, False,
signing_priv_key_pem, session)
if not profile_json:
return None
personUrl = profile_json['id']
searchDomain, searchPort = get_domain_from_actor(personUrl)
if not searchDomain:
return None
searchNickname = get_nickname_from_actor(personUrl)
if not searchNickname:
return None
searchDomainFull = get_full_domain(searchDomain, searchPort)
profileStr = ''
cssFilename = base_dir + '/epicyon-profile.css'
if os.path.isfile(base_dir + '/epicyon.css'):
cssFilename = base_dir + '/epicyon.css'
isGroup = False
if profile_json.get('type'):
if profile_json['type'] == 'Group':
isGroup = True
avatarUrl = ''
if profile_json.get('icon'):
if profile_json['icon'].get('url'):
avatarUrl = profile_json['icon']['url']
if not avatarUrl:
avatarUrl = get_person_avatar_url(base_dir, personUrl,
person_cache, True)
displayName = searchNickname
if profile_json.get('name'):
displayName = profile_json['name']
lockedAccount = get_locked_account(profile_json)
if lockedAccount:
displayName += '🔒'
movedTo = ''
if profile_json.get('movedTo'):
movedTo = profile_json['movedTo']
if '"' in movedTo:
movedTo = movedTo.split('"')[1]
displayName += ''
followsYou = \
is_follower_of_person(base_dir,
nickname, domain,
searchNickname,
searchDomainFull)
profileDescription = ''
if profile_json.get('summary'):
profileDescription = profile_json['summary']
outboxUrl = None
if not profile_json.get('outbox'):
if debug:
pprint(profile_json)
print('DEBUG: No outbox found')
return None
outboxUrl = profile_json['outbox']
# profileBackgroundImage = ''
# if profile_json.get('image'):
# if profile_json['image'].get('url'):
# profileBackgroundImage = profile_json['image']['url']
# url to return to
backUrl = path
if not backUrl.endswith('/inbox'):
backUrl += '/inbox'
profileDescriptionShort = profileDescription
if '\n' in profileDescription:
if len(profileDescription.split('\n')) > 2:
profileDescriptionShort = ''
else:
if '<br>' in profileDescription:
if len(profileDescription.split('<br>')) > 2:
profileDescriptionShort = ''
# keep the profile description short
if len(profileDescriptionShort) > 256:
profileDescriptionShort = ''
# remove formatting from profile description used on title
avatarDescription = ''
if profile_json.get('summary'):
if isinstance(profile_json['summary'], str):
avatarDescription = \
profile_json['summary'].replace('<br>', '\n')
avatarDescription = avatarDescription.replace('<p>', '')
avatarDescription = avatarDescription.replace('</p>', '')
if '<' in avatarDescription:
avatarDescription = remove_html(avatarDescription)
imageUrl = ''
if profile_json.get('image'):
if profile_json['image'].get('url'):
imageUrl = profile_json['image']['url']
alsoKnownAs = None
if profile_json.get('alsoKnownAs'):
alsoKnownAs = profile_json['alsoKnownAs']
joinedDate = None
if profile_json.get('published'):
if 'T' in profile_json['published']:
joinedDate = profile_json['published']
profileStr = \
_get_profile_header_after_search(base_dir,
nickname, defaultTimeline,
searchNickname,
searchDomainFull,
translate,
displayName, followsYou,
profileDescriptionShort,
avatarUrl, imageUrl,
movedTo, profile_json['id'],
alsoKnownAs, accessKeys,
joinedDate)
domain_full = get_full_domain(domain, port)
followIsPermitted = True
if not profile_json.get('followers'):
# no followers collection specified within actor
followIsPermitted = False
elif searchNickname == 'news' and searchDomainFull == domain_full:
# currently the news actor is not something you can follow
followIsPermitted = False
elif searchNickname == nickname and searchDomainFull == domain_full:
# don't follow yourself!
followIsPermitted = False
if followIsPermitted:
followStr = 'Follow'
if isGroup:
followStr = 'Join'
profileStr += \
'<div class="container">\n' + \
' <form method="POST" action="' + \
backUrl + '/followconfirm">\n' + \
' <center>\n' + \
' <input type="hidden" name="actor" value="' + \
personUrl + '">\n' + \
' <button type="submit" class="button" name="submitYes" ' + \
'accesskey="' + accessKeys['followButton'] + '">' + \
translate[followStr] + '</button>\n' + \
' <button type="submit" class="button" name="submitView" ' + \
'accesskey="' + accessKeys['viewButton'] + '">' + \
translate['View'] + '</button>\n' + \
' </center>\n' + \
' </form>\n' + \
'</div>\n'
else:
profileStr += \
'<div class="container">\n' + \
' <form method="POST" action="' + \
backUrl + '/followconfirm">\n' + \
' <center>\n' + \
' <input type="hidden" name="actor" value="' + \
personUrl + '">\n' + \
' <button type="submit" class="button" name="submitView" ' + \
'accesskey="' + accessKeys['viewButton'] + '">' + \
translate['View'] + '</button>\n' + \
' </center>\n' + \
' </form>\n' + \
'</div>\n'
userFeed = \
parse_user_feed(signing_priv_key_pem,
session, outboxUrl, asHeader, project_version,
http_prefix, domain, debug)
if userFeed:
i = 0
for item in userFeed:
showItem, post_json_object = \
_valid_profile_preview_post(item, personUrl)
if not showItem:
continue
profileStr += \
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, avatarUrl,
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)
i += 1
if i >= 8:
break
instanceTitle = \
get_config_param(base_dir, 'instanceTitle')
return html_header_with_external_style(cssFilename,
instanceTitle, None) + \
profileStr + html_footer()
def _get_profile_header(base_dir: str, http_prefix: str,
nickname: str, domain: str,
domain_full: str, translate: {},
defaultTimeline: str,
displayName: str,
avatarDescription: str,
profileDescriptionShort: str,
loginButton: str, avatarUrl: str,
theme: str, movedTo: str,
alsoKnownAs: [],
pinnedContent: str,
accessKeys: {},
joinedDate: str,
occupationName: str) -> str:
"""The header of the profile screen, containing background
image and avatar
"""
htmlStr = \
'\n\n <figure class="profileHeader">\n' + \
' <a href="/users/' + \
nickname + '/' + defaultTimeline + '" title="' + \
translate['Switch to timeline view'] + '">\n' + \
' <img class="profileBackground" ' + \
'alt="" ' + \
'src="/users/' + nickname + '/image_' + theme + '.png" /></a>\n' + \
' <figcaption>\n' + \
' <a href="/users/' + \
nickname + '/' + defaultTimeline + '" title="' + \
translate['Switch to timeline view'] + '">\n' + \
' <img loading="lazy" src="' + avatarUrl + '" ' + \
'alt="" class="title"></a>\n'
occupationStr = ''
if occupationName:
occupationStr += \
' <b>' + occupationName + '</b><br>\n'
htmlStr += ' <h1>' + displayName + '</h1>\n' + occupationStr
htmlStr += \
' <p><b>@' + nickname + '@' + domain_full + '</b><br>\n'
if joinedDate:
htmlStr += \
' <p>' + translate['Joined'] + ' ' + \
joinedDate.split('T')[0] + '<br>\n'
if movedTo:
newNickname = get_nickname_from_actor(movedTo)
newDomain, newPort = get_domain_from_actor(movedTo)
newDomainFull = get_full_domain(newDomain, newPort)
if newNickname and newDomain:
htmlStr += \
' <p>' + translate['New account'] + ': ' + \
'<a href="' + movedTo + '">@' + \
newNickname + '@' + newDomainFull + '</a><br>\n'
elif alsoKnownAs:
otherAccountsHtml = \
' <p>' + translate['Other accounts'] + ': '
actor = local_actor_url(http_prefix, nickname, domain_full)
ctr = 0
if isinstance(alsoKnownAs, list):
for altActor in alsoKnownAs:
if altActor == actor:
continue
if ctr > 0:
otherAccountsHtml += ' '
ctr += 1
altDomain, altPort = get_domain_from_actor(altActor)
otherAccountsHtml += \
'<a href="' + altActor + '">' + altDomain + '</a>'
elif isinstance(alsoKnownAs, str):
if alsoKnownAs != actor:
ctr += 1
altDomain, altPort = get_domain_from_actor(alsoKnownAs)
otherAccountsHtml += \
'<a href="' + alsoKnownAs + '">' + altDomain + '</a>'
otherAccountsHtml += '</p>\n'
if ctr > 0:
htmlStr += otherAccountsHtml
htmlStr += \
' <a href="/users/' + nickname + \
'/qrcode.png" alt="' + translate['QR Code'] + '" title="' + \
translate['QR Code'] + '">' + \
'<img class="qrcode" alt="' + translate['QR Code'] + \
'" src="/icons/qrcode.png" /></a></p>\n' + \
' <p>' + profileDescriptionShort + '</p>\n' + loginButton
if pinnedContent:
htmlStr += pinnedContent.replace('<p>', '<p>📎', 1)
htmlStr += \
' </figcaption>\n' + \
' </figure>\n\n'
return htmlStr
def _get_profile_header_after_search(base_dir: str,
nickname: str, defaultTimeline: str,
searchNickname: str,
searchDomainFull: str,
translate: {},
displayName: str,
followsYou: bool,
profileDescriptionShort: str,
avatarUrl: str, imageUrl: str,
movedTo: str, actor: str,
alsoKnownAs: [],
accessKeys: {},
joinedDate: str) -> str:
"""The header of a searched for handle, containing background
image and avatar
"""
if not imageUrl:
imageUrl = '/defaultprofilebackground'
htmlStr = \
'\n\n <figure class="profileHeader">\n' + \
' <a href="/users/' + \
nickname + '/' + defaultTimeline + '" title="' + \
translate['Switch to timeline view'] + '" ' + \
'accesskey="' + accessKeys['menuTimeline'] + '">\n' + \
' <img class="profileBackground" ' + \
'alt="" ' + \
'src="' + imageUrl + '" /></a>\n' + \
' <figcaption>\n'
if avatarUrl:
htmlStr += \
' <a href="/users/' + \
nickname + '/' + defaultTimeline + '" title="' + \
translate['Switch to timeline view'] + '">\n' + \
' <img loading="lazy" src="' + avatarUrl + '" ' + \
'alt="" class="title"></a>\n'
if not displayName:
displayName = searchNickname
htmlStr += \
' <h1>' + displayName + '</h1>\n' + \
' <p><b>@' + searchNickname + '@' + searchDomainFull + '</b><br>\n'
if joinedDate:
htmlStr += ' <p>' + translate['Joined'] + ' ' + \
joinedDate.split('T')[0] + '</p>\n'
if followsYou:
htmlStr += ' <p><b>' + translate['Follows you'] + '</b></p>\n'
if movedTo:
newNickname = get_nickname_from_actor(movedTo)
newDomain, newPort = get_domain_from_actor(movedTo)
newDomainFull = get_full_domain(newDomain, newPort)
if newNickname and newDomain:
newHandle = newNickname + '@' + newDomainFull
htmlStr += ' <p>' + translate['New account'] + \
': <a href="' + movedTo + '">@' + newHandle + '</a></p>\n'
elif alsoKnownAs:
otherAccountshtml = \
' <p>' + translate['Other accounts'] + ': '
ctr = 0
if isinstance(alsoKnownAs, list):
for altActor in alsoKnownAs:
if altActor == actor:
continue
if ctr > 0:
otherAccountshtml += ' '
ctr += 1
altDomain, altPort = get_domain_from_actor(altActor)
otherAccountshtml += \
'<a href="' + altActor + '">' + altDomain + '</a>'
elif isinstance(alsoKnownAs, str):
if alsoKnownAs != actor:
ctr += 1
altDomain, altPort = get_domain_from_actor(alsoKnownAs)
otherAccountshtml += \
'<a href="' + alsoKnownAs + '">' + altDomain + '</a>'
otherAccountshtml += '</p>\n'
if ctr > 0:
htmlStr += otherAccountshtml
htmlStr += \
' <p>' + profileDescriptionShort + '</p>\n' + \
' </figcaption>\n' + \
' </figure>\n\n'
return htmlStr
def html_profile(signing_priv_key_pem: str,
rss_icon_at_top: bool,
css_cache: {}, icons_as_buttons: bool,
defaultTimeline: 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, accessKeys: {}, city: str,
system_language: str, max_like_count: int,
shared_items_federated_domains: [],
extraJson: {}, pageNumber: int,
maxItemsPerPage: int,
cw_lists: {}, lists_enabled: str,
content_license_url: str) -> str:
"""Show the profile page as html
"""
nickname = profile_json['preferredUsername']
if not nickname:
return ""
if is_system_account(nickname):
return html_front_screen(signing_priv_key_pem,
rss_icon_at_top,
css_cache, icons_as_buttons,
defaultTimeline,
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, extraJson,
allow_local_network_access, accessKeys,
system_language, max_like_count,
shared_items_federated_domains, None,
pageNumber, maxItemsPerPage, cw_lists,
lists_enabled)
domain, port = get_domain_from_actor(profile_json['id'])
if not domain:
return ""
displayName = \
add_emoji_to_display_name(session, base_dir, http_prefix,
nickname, domain,
profile_json['name'], True)
domain_full = get_full_domain(domain, port)
profileDescription = \
add_emoji_to_display_name(session, base_dir, http_prefix,
nickname, domain,
profile_json['summary'], False)
postsButton = 'button'
followingButton = 'button'
followersButton = 'button'
rolesButton = 'button'
skillsButton = 'button'
sharesButton = 'button'
wantedButton = 'button'
if selected == 'posts':
postsButton = 'buttonselected'
elif selected == 'following':
followingButton = 'buttonselected'
elif selected == 'followers':
followersButton = 'buttonselected'
elif selected == 'roles':
rolesButton = 'buttonselected'
elif selected == 'skills':
skillsButton = 'buttonselected'
elif selected == 'shares':
sharesButton = 'buttonselected'
elif selected == 'wanted':
wantedButton = 'buttonselected'
loginButton = ''
followApprovalsSection = ''
followApprovals = False
editProfileStr = ''
logoutStr = ''
actor = profile_json['id']
usersPath = '/users/' + actor.split('/users/')[1]
donateSection = ''
donateUrl = get_donation_url(profile_json)
websiteUrl = get_website(profile_json, translate)
blogAddress = get_blog_address(profile_json)
EnigmaPubKey = get_enigma_pub_key(profile_json)
PGPpubKey = get_pgp_pub_key(profile_json)
PGPfingerprint = get_pgp_fingerprint(profile_json)
emailAddress = get_email_address(profile_json)
xmppAddress = get_xmpp_address(profile_json)
matrixAddress = get_matrix_address(profile_json)
ssbAddress = get_ssb_address(profile_json)
toxAddress = get_tox_address(profile_json)
briarAddress = get_briar_address(profile_json)
jamiAddress = get_jami_address(profile_json)
cwtchAddress = get_cwtch_address(profile_json)
if donateUrl or websiteUrl or xmppAddress or matrixAddress or \
ssbAddress or toxAddress or briarAddress or \
jamiAddress or cwtchAddress or PGPpubKey or EnigmaPubKey or \
PGPfingerprint or emailAddress:
donateSection = '<div class="container">\n'
donateSection += ' <center>\n'
if donateUrl and not is_system_account(nickname):
donateSection += \
' <p><a href="' + donateUrl + \
'"><button class="donateButton">' + translate['Donate'] + \
'</button></a></p>\n'
if websiteUrl:
donateSection += \
'<p>' + translate['Website'] + ': <a href="' + \
websiteUrl + '">' + websiteUrl + '</a></p>\n'
if emailAddress:
donateSection += \
'<p>' + translate['Email'] + ': <a href="mailto:' + \
emailAddress + '">' + emailAddress + '</a></p>\n'
if blogAddress:
donateSection += \
'<p>Blog: <a href="' + \
blogAddress + '">' + blogAddress + '</a></p>\n'
if xmppAddress:
donateSection += \
'<p>' + translate['XMPP'] + ': <a href="xmpp:' + \
xmppAddress + '">' + xmppAddress + '</a></p>\n'
if matrixAddress:
donateSection += \
'<p>' + translate['Matrix'] + ': ' + matrixAddress + '</p>\n'
if ssbAddress:
donateSection += \
'<p>SSB: <label class="ssbaddr">' + \
ssbAddress + '</label></p>\n'
if toxAddress:
donateSection += \
'<p>Tox: <label class="toxaddr">' + \
toxAddress + '</label></p>\n'
if briarAddress:
if briarAddress.startswith('briar://'):
donateSection += \
'<p><label class="toxaddr">' + \
briarAddress + '</label></p>\n'
else:
donateSection += \
'<p>briar://<label class="toxaddr">' + \
briarAddress + '</label></p>\n'
if jamiAddress:
donateSection += \
'<p>Jami: <label class="toxaddr">' + \
jamiAddress + '</label></p>\n'
if cwtchAddress:
donateSection += \
'<p>Cwtch: <label class="toxaddr">' + \
cwtchAddress + '</label></p>\n'
if EnigmaPubKey:
donateSection += \
'<p>Enigma: <label class="toxaddr">' + \
EnigmaPubKey + '</label></p>\n'
if PGPfingerprint:
donateSection += \
'<p class="pgp">PGP: ' + \
PGPfingerprint.replace('\n', '<br>') + '</p>\n'
if PGPpubKey:
donateSection += \
'<p class="pgp">' + PGPpubKey.replace('\n', '<br>') + '</p>\n'
donateSection += ' </center>\n'
donateSection += '</div>\n'
if authorized:
editProfileStr = \
'<a class="imageAnchor" href="' + usersPath + '/editprofile">' + \
'<img loading="lazy" src="/icons' + \
'/edit.png" title="' + translate['Edit'] + \
'" alt="| ' + translate['Edit'] + '" class="timelineicon"/></a>\n'
logoutStr = \
'<a class="imageAnchor" href="/logout">' + \
'<img loading="lazy" src="/icons' + \
'/logout.png" title="' + translate['Logout'] + \
'" alt="| ' + translate['Logout'] + \
'" class="timelineicon"/></a>\n'
# are there any follow requests?
followRequestsFilename = \
acct_dir(base_dir, nickname, domain) + '/followrequests.txt'
if os.path.isfile(followRequestsFilename):
with open(followRequestsFilename, 'r') as f:
for line in f:
if len(line) > 0:
followApprovals = True
followersButton = 'buttonhighlighted'
if selected == 'followers':
followersButton = 'buttonselectedhighlighted'
break
if selected == 'followers':
if followApprovals:
currFollowerDomains = \
get_follower_domains(base_dir, nickname, domain)
with open(followRequestsFilename, 'r') as f:
for followerHandle in f:
if len(line) > 0:
followerHandle = followerHandle.replace('\n', '')
if '://' in followerHandle:
followerActor = followerHandle
else:
nick = followerHandle.split('@')[0]
dom = followerHandle.split('@')[1]
followerActor = \
local_actor_url(http_prefix, nick, dom)
# is this a new domain?
# if so then append a new instance indicator
followerDomain, _ = \
get_domain_from_actor(followerActor)
newFollowerDomain = ''
if followerDomain not in currFollowerDomains:
newFollowerDomain = ''
basePath = '/users/' + nickname
followApprovalsSection += '<div class="container">'
followApprovalsSection += \
'<a href="' + followerActor + '">'
followApprovalsSection += \
'<span class="followRequestHandle">' + \
followerHandle + \
newFollowerDomain + '</span></a>'
# show Approve and Deny buttons
followApprovalsSection += \
'<a href="' + basePath + \
'/followapprove=' + followerHandle + '">'
followApprovalsSection += \
'<button class="followApprove">' + \
translate['Approve'] + '</button></a><br><br>'
followApprovalsSection += \
'<a href="' + basePath + \
'/followdeny=' + followerHandle + '">'
followApprovalsSection += \
'<button class="followDeny">' + \
translate['Deny'] + '</button></a>'
followApprovalsSection += '</div>'
profileDescriptionShort = profileDescription
if '\n' in profileDescription:
if len(profileDescription.split('\n')) > 2:
profileDescriptionShort = ''
else:
if '<br>' in profileDescription:
if len(profileDescription.split('<br>')) > 2:
profileDescriptionShort = ''
profileDescription = profileDescription.replace('<br>', '\n')
# keep the profile description short
if len(profileDescriptionShort) > 256:
profileDescriptionShort = ''
# remove formatting from profile description used on title
avatarDescription = ''
if profile_json.get('summary'):
avatarDescription = profile_json['summary'].replace('<br>', '\n')
avatarDescription = avatarDescription.replace('<p>', '')
avatarDescription = avatarDescription.replace('</p>', '')
movedTo = ''
if profile_json.get('movedTo'):
movedTo = profile_json['movedTo']
if '"' in movedTo:
movedTo = movedTo.split('"')[1]
alsoKnownAs = None
if profile_json.get('alsoKnownAs'):
alsoKnownAs = profile_json['alsoKnownAs']
joinedDate = None
if profile_json.get('published'):
if 'T' in profile_json['published']:
joinedDate = profile_json['published']
occupationName = None
if profile_json.get('hasOccupation'):
occupationName = get_occupation_name(profile_json)
avatarUrl = profile_json['icon']['url']
# use alternate path for local avatars to avoid any caching issues
if '://' + domain_full + '/system/accounts/avatars/' in avatarUrl:
avatarUrl = \
avatarUrl.replace('://' + domain_full +
'/system/accounts/avatars/',
'://' + domain_full + '/users/')
# get pinned post content
accountDir = acct_dir(base_dir, nickname, domain)
pinnedFilename = accountDir + '/pinToProfile.txt'
pinnedContent = None
if os.path.isfile(pinnedFilename):
with open(pinnedFilename, 'r') as pinFile:
pinnedContent = pinFile.read()
profileHeaderStr = \
_get_profile_header(base_dir, http_prefix,
nickname, domain,
domain_full, translate,
defaultTimeline, displayName,
avatarDescription,
profileDescriptionShort,
loginButton, avatarUrl, theme,
movedTo, alsoKnownAs,
pinnedContent, accessKeys,
joinedDate, occupationName)
# keyboard navigation
userPathStr = '/users/' + nickname
deft = defaultTimeline
isGroup = False
followersStr = translate['Followers']
if is_group_account(base_dir, nickname, domain):
isGroup = True
followersStr = translate['Members']
menuTimeline = \
html_hide_from_screen_reader('🏠') + ' ' + \
translate['Switch to timeline view']
menuEdit = \
html_hide_from_screen_reader('') + ' ' + translate['Edit']
if not isGroup:
menuFollowing = \
html_hide_from_screen_reader('👥') + ' ' + translate['Following']
menuFollowers = \
html_hide_from_screen_reader('👪') + ' ' + followersStr
if not isGroup:
menuRoles = \
html_hide_from_screen_reader('🤚') + ' ' + translate['Roles']
menuSkills = \
html_hide_from_screen_reader('🛠') + ' ' + translate['Skills']
menuLogout = \
html_hide_from_screen_reader('') + ' ' + translate['Logout']
navLinks = {
menuTimeline: userPathStr + '/' + deft,
menuEdit: userPathStr + '/editprofile',
menuFollowing: userPathStr + '/following#timeline',
menuFollowers: userPathStr + '/followers#timeline',
menuRoles: userPathStr + '/roles#timeline',
menuSkills: userPathStr + '/skills#timeline',
menuLogout: '/logout'
}
if is_artist(base_dir, nickname):
menuThemeDesigner = \
html_hide_from_screen_reader('🎨') + ' ' + \
translate['Theme Designer']
navLinks[menuThemeDesigner] = userPathStr + '/themedesigner'
navAccessKeys = {}
for variableName, key in accessKeys.items():
if not locals().get(variableName):
continue
navAccessKeys[locals()[variableName]] = key
profileStr = html_keyboard_navigation(text_mode_banner,
navLinks, navAccessKeys)
profileStr += profileHeaderStr + donateSection
profileStr += '<div class="container" id="buttonheader">\n'
profileStr += ' <center>'
profileStr += \
' <a href="' + usersPath + '#buttonheader"><button class="' + \
postsButton + '"><span>' + translate['Posts'] + \
' </span></button></a>'
if not isGroup:
profileStr += \
' <a href="' + usersPath + '/following#buttonheader">' + \
'<button class="' + followingButton + '"><span>' + \
translate['Following'] + ' </span></button></a>'
profileStr += \
' <a href="' + usersPath + '/followers#buttonheader">' + \
'<button class="' + followersButton + \
'"><span>' + followersStr + ' </span></button></a>'
if not isGroup:
profileStr += \
' <a href="' + usersPath + '/roles#buttonheader">' + \
'<button class="' + rolesButton + '"><span>' + \
translate['Roles'] + \
' </span></button></a>'
profileStr += \
' <a href="' + usersPath + '/skills#buttonheader">' + \
'<button class="' + skillsButton + '"><span>' + \
translate['Skills'] + ' </span></button></a>'
# profileStr += \
# ' <a href="' + usersPath + '/shares#buttonheader">' + \
# '<button class="' + sharesButton + '"><span>' + \
# translate['Shares'] + ' </span></button></a>'
# profileStr += \
# ' <a href="' + usersPath + '/wanted#buttonheader">' + \
# '<button class="' + wantedButton + '"><span>' + \
# translate['Wanted'] + ' </span></button></a>'
profileStr += logoutStr + editProfileStr
profileStr += ' </center>'
profileStr += '</div>'
# start of #timeline
profileStr += '<div id="timeline">\n'
profileStr += followApprovalsSection
cssFilename = base_dir + '/epicyon-profile.css'
if os.path.isfile(base_dir + '/epicyon.css'):
cssFilename = base_dir + '/epicyon.css'
licenseStr = \
'<a href="https://gitlab.com/bashrc2/epicyon">' + \
'<img loading="lazy" class="license" alt="' + \
translate['Get the source code'] + '" title="' + \
translate['Get the source code'] + '" src="/icons/agpl.png" /></a>'
if selected == 'posts':
profileStr += \
_html_profile_posts(recent_posts_cache, max_recent_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) + licenseStr
if not isGroup:
if selected == 'following':
profileStr += \
_html_profile_following(translate, base_dir, http_prefix,
authorized, nickname,
domain, port, session,
cached_webfingers,
person_cache, extraJson,
project_version, ["unfollow"],
selected,
usersPath, pageNumber, maxItemsPerPage,
dormant_months, debug,
signing_priv_key_pem)
if selected == 'followers':
profileStr += \
_html_profile_following(translate, base_dir, http_prefix,
authorized, nickname,
domain, port, session,
cached_webfingers,
person_cache, extraJson,
project_version, ["block"],
selected, usersPath, pageNumber,
maxItemsPerPage, dormant_months, debug,
signing_priv_key_pem)
if not isGroup:
if selected == 'roles':
profileStr += \
_html_profile_roles(translate, nickname, domain_full,
extraJson)
elif selected == 'skills':
profileStr += \
_html_profile_skills(translate, nickname, domain_full,
extraJson)
# elif selected == 'shares':
# profileStr += \
# _html_profile_shares(actor, translate,
# nickname, domain_full,
# extraJson, 'shares') + licenseStr
# elif selected == 'wanted':
# profileStr += \
# _html_profile_shares(actor, translate,
# nickname, domain_full,
# extraJson, 'wanted') + licenseStr
# end of #timeline
profileStr += '</div>'
instanceTitle = \
get_config_param(base_dir, 'instanceTitle')
profileStr = \
html_header_with_person_markup(cssFilename, instanceTitle,
profile_json, city,
content_license_url) + \
profileStr + html_footer()
return profileStr
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) -> str:
"""Shows posts on the profile screen
These should only be public posts
"""
separatorStr = html_post_separator(base_dir, None)
profileStr = ''
maxItems = 4
ctr = 0
currPage = 1
boxName = 'outbox'
while ctr < maxItems and currPage < 4:
outboxFeedPathStr = \
'/users/' + nickname + '/' + boxName + '?page=' + \
str(currPage)
outboxFeed = \
person_box_json({}, session, base_dir, domain,
port,
outboxFeedPathStr,
http_prefix,
10, boxName,
authorized, 0, False, 0)
if not outboxFeed:
break
if len(outboxFeed['orderedItems']) == 0:
break
for item in outboxFeed['orderedItems']:
if item['type'] == 'Create':
postStr = \
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)
if postStr:
profileStr += postStr + separatorStr
ctr += 1
if ctr >= maxItems:
break
currPage += 1
return profileStr
def _html_profile_following(translate: {}, base_dir: str, http_prefix: str,
authorized: bool,
nickname: str, domain: str, port: int,
session, cached_webfingers: {}, person_cache: {},
followingJson: {}, project_version: str,
buttons: [],
feedName: str, actor: str,
pageNumber: int,
maxItemsPerPage: int,
dormant_months: int, debug: bool,
signing_priv_key_pem: str) -> str:
"""Shows following on the profile screen
"""
profileStr = ''
if authorized and pageNumber:
if authorized and pageNumber > 1:
# page up arrow
profileStr += \
' <center>\n' + \
' <a href="' + actor + '/' + feedName + \
'?page=' + str(pageNumber - 1) + '#buttonheader' + \
'"><img loading="lazy" class="pageicon" src="/' + \
'icons/pageup.png" title="' + \
translate['Page up'] + '" alt="' + \
translate['Page up'] + '"></a>\n' + \
' </center>\n'
for followingActor in followingJson['orderedItems']:
# is this a dormant followed account?
dormant = False
if authorized and feedName == 'following':
dormant = \
is_dormant(base_dir, nickname, domain, followingActor,
dormant_months)
profileStr += \
_individual_follow_as_html(signing_priv_key_pem,
translate, base_dir, session,
cached_webfingers, person_cache,
domain, followingActor,
authorized, nickname,
http_prefix, project_version, dormant,
debug, buttons)
if authorized and maxItemsPerPage and pageNumber:
if len(followingJson['orderedItems']) >= maxItemsPerPage:
# page down arrow
profileStr += \
' <center>\n' + \
' <a href="' + actor + '/' + feedName + \
'?page=' + str(pageNumber + 1) + '#buttonheader' + \
'"><img loading="lazy" class="pageicon" src="/' + \
'icons/pagedown.png" title="' + \
translate['Page down'] + '" alt="' + \
translate['Page down'] + '"></a>\n' + \
' </center>\n'
return profileStr
def _html_profile_roles(translate: {}, nickname: str, domain: str,
rolesList: []) -> str:
"""Shows roles on the profile screen
"""
profileStr = ''
profileStr += \
'<div class="roles">\n<div class="roles-inner">\n'
for role in rolesList:
if translate.get(role):
profileStr += '<h3>' + translate[role] + '</h3>\n'
else:
profileStr += '<h3>' + role + '</h3>\n'
profileStr += '</div></div>\n'
if len(profileStr) == 0:
profileStr += \
'<p>@' + nickname + '@' + domain + ' has no roles assigned</p>\n'
else:
profileStr = '<div>' + profileStr + '</div>\n'
return profileStr
def _html_profile_skills(translate: {}, nickname: str, domain: str,
skillsJson: {}) -> str:
"""Shows skills on the profile screen
"""
profileStr = ''
for skill, level in skillsJson.items():
profileStr += \
'<div>' + skill + \
'<br><div id="myProgress"><div id="myBar" style="width:' + \
str(level) + '%"></div></div></div>\n<br>\n'
if len(profileStr) > 0:
profileStr = '<center><div class="skill-title">' + \
profileStr + '</div></center>\n'
return profileStr
def _html_profile_shares(actor: str, translate: {},
nickname: str, domain: str, sharesJson: {},
sharesFileType: str) -> str:
"""Shows shares on the profile screen
"""
profileStr = ''
for item in sharesJson['orderedItems']:
profileStr += html_individual_share(domain, item['shareId'],
actor, item, translate,
False, False,
sharesFileType)
if len(profileStr) > 0:
profileStr = '<div class="share-title">' + profileStr + '</div>\n'
return profileStr
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)
themesDropdown = ' <label class="labels">' + \
translate['Theme'] + '</label><br>\n'
grayscale = _grayscale_enabled(base_dir)
themesDropdown += \
edit_check_box(translate['Grayscale'], 'grayscale', grayscale)
themesDropdown += ' <select id="themeDropdown" ' + \
'name="themeDropdown" class="theme">'
for theme_name in themes:
translatedThemeName = theme_name
if translate.get(theme_name):
translatedThemeName = translate[theme_name]
themesDropdown += ' <option value="' + \
theme_name.lower() + '">' + \
translatedThemeName + '</option>'
themesDropdown += ' </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'):
themesDropdown += \
edit_check_box(translate['Remove the custom font'],
'removeCustomFont', False)
theme_name = get_config_param(base_dir, 'theme')
themesDropdown = \
themesDropdown.replace('<option value="' + theme_name + '">',
'<option value="' + theme_name +
'" selected>')
return themesDropdown
def _html_edit_profile_graphic_design(base_dir: str, translate: {}) -> str:
"""Graphic design section on Edit Profile screen
"""
themeFormats = '.zip, .gz'
graphicsStr = begin_edit_section(translate['Graphic Design'])
low_bandwidth = get_config_param(base_dir, 'low_bandwidth')
if not low_bandwidth:
low_bandwidth = False
graphicsStr += _html_themes_dropdown(base_dir, translate)
graphicsStr += \
' <label class="labels">' + \
translate['Import Theme'] + '</label>\n'
graphicsStr += ' <input type="file" id="import_theme" '
graphicsStr += 'name="submitImportTheme" '
graphicsStr += 'accept="' + themeFormats + '">\n'
graphicsStr += \
' <label class="labels">' + \
translate['Export Theme'] + '</label><br>\n'
graphicsStr += \
' <button type="submit" class="button" ' + \
'name="submitExportTheme">➤</button><br>\n'
graphicsStr += \
edit_check_box(translate['Low Bandwidth'], 'low_bandwidth',
bool(low_bandwidth))
graphicsStr += end_edit_section()
return graphicsStr
def _html_edit_profile_twitter(base_dir: str, translate: {},
removeTwitter: str) -> str:
"""Edit twitter settings within profile
"""
# Twitter section
twitterStr = begin_edit_section(translate['Twitter'])
twitterStr += \
edit_check_box(translate['Remove Twitter posts'],
'removeTwitter', removeTwitter)
twitter_replacement_domain = get_config_param(base_dir, "twitterdomain")
if not twitter_replacement_domain:
twitter_replacement_domain = ''
twitterStr += \
edit_text_field(translate['Twitter Replacement Domain'],
'twitterdomain', twitter_replacement_domain)
twitterStr += end_edit_section()
return twitterStr
def _html_edit_profile_instance(base_dir: str, translate: {},
peertube_instances: [],
media_instanceStr: str,
blogs_instanceStr: str,
news_instanceStr: str) -> (str, str, str, str):
"""Edit profile instance settings
"""
imageFormats = get_image_formats()
# Instance details section
instanceDescription = \
get_config_param(base_dir, 'instanceDescription')
customSubmitText = \
get_config_param(base_dir, 'customSubmitText')
instanceDescriptionShort = \
get_config_param(base_dir, 'instanceDescriptionShort')
instanceTitle = \
get_config_param(base_dir, 'instanceTitle')
content_license_url = \
get_config_param(base_dir, 'content_license_url')
if not content_license_url:
content_license_url = 'https://creativecommons.org/licenses/by/4.0'
instanceStr = begin_edit_section(translate['Instance Settings'])
instanceStr += \
edit_text_field(translate['Instance Title'],
'instanceTitle', instanceTitle)
instanceStr += '<br>\n'
instanceStr += \
edit_text_field(translate['Instance Short Description'],
'instanceDescriptionShort', instanceDescriptionShort)
instanceStr += '<br>\n'
instanceStr += \
edit_text_area(translate['Instance Description'],
'instanceDescription', instanceDescription, 200,
'', True)
instanceStr += \
edit_text_field(translate['Content License'],
'content_license_url', content_license_url)
instanceStr += '<br>\n'
instanceStr += \
edit_text_field(translate['Custom post submit button text'],
'customSubmitText', customSubmitText)
instanceStr += '<br>\n'
instanceStr += \
' <label class="labels">' + \
translate['Instance Logo'] + '</label>' + \
' <input type="file" id="instanceLogo" name="instanceLogo"' + \
' accept="' + imageFormats + '"><br>\n' + \
' <br><label class="labels">' + \
translate['Security'] + '</label><br>\n'
nodeInfoStr = \
translate['Show numbers of accounts within instance metadata']
if get_config_param(base_dir, "show_node_info_accounts"):
instanceStr += \
edit_check_box(nodeInfoStr, 'show_node_info_accounts', True)
else:
instanceStr += \
edit_check_box(nodeInfoStr, 'show_node_info_accounts', False)
nodeInfoStr = \
translate['Show version number within instance metadata']
if get_config_param(base_dir, "show_node_info_version"):
instanceStr += \
edit_check_box(nodeInfoStr, 'show_node_info_version', True)
else:
instanceStr += \
edit_check_box(nodeInfoStr, 'show_node_info_version', False)
if get_config_param(base_dir, "verify_all_signatures"):
instanceStr += \
edit_check_box(translate['Verify all signatures'],
'verifyallsignatures', True)
else:
instanceStr += \
edit_check_box(translate['Verify all signatures'],
'verifyallsignatures', False)
instanceStr += translate['Enabling broch mode'] + '<br>\n'
if get_config_param(base_dir, "broch_mode"):
instanceStr += \
edit_check_box(translate['Broch mode'], 'broch_mode', True)
else:
instanceStr += \
edit_check_box(translate['Broch mode'], 'broch_mode', False)
# Instance type
instanceStr += \
' <br><label class="labels">' + \
translate['Type of instance'] + '</label><br>\n'
instanceStr += \
edit_check_box(translate['This is a media instance'],
'media_instance', media_instanceStr)
instanceStr += \
edit_check_box(translate['This is a blogging instance'],
'blogs_instance', blogs_instanceStr)
instanceStr += \
edit_check_box(translate['This is a news instance'],
'news_instance', news_instanceStr)
instanceStr += end_edit_section()
# Role assignments section
moderators = ''
moderatorsFile = base_dir + '/accounts/moderators.txt'
if os.path.isfile(moderatorsFile):
with open(moderatorsFile, 'r') as f:
moderators = f.read()
# site moderators
roleAssignStr = \
begin_edit_section(translate['Role Assignment']) + \
' <b><label class="labels">' + \
translate['Moderators'] + '</label></b><br>\n' + \
' ' + \
translate['A list of moderator nicknames. One per line.'] + \
' <textarea id="message" name="moderators" placeholder="' + \
translate['List of moderator nicknames'] + \
'..." style="height:200px" spellcheck="false">' + \
moderators + '</textarea>'
# site editors
editors = ''
editorsFile = base_dir + '/accounts/editors.txt'
if os.path.isfile(editorsFile):
with open(editorsFile, 'r') as f:
editors = f.read()
roleAssignStr += \
' <b><label class="labels">' + \
translate['Site Editors'] + '</label></b><br>\n' + \
' ' + \
translate['A list of editor nicknames. One per line.'] + \
' <textarea id="message" name="editors" placeholder="" ' + \
'style="height:200px" spellcheck="false">' + \
editors + '</textarea>'
# counselors
counselors = ''
counselorsFile = base_dir + '/accounts/counselors.txt'
if os.path.isfile(counselorsFile):
with open(counselorsFile, 'r') as f:
counselors = f.read()
roleAssignStr += \
edit_text_area(translate['Counselors'], 'counselors', counselors,
200, '', False)
# artists
artists = ''
artistsFile = base_dir + '/accounts/artists.txt'
if os.path.isfile(artistsFile):
with open(artistsFile, 'r') as f:
artists = f.read()
roleAssignStr += \
edit_text_area(translate['Artists'], 'artists', artists,
200, '', False)
roleAssignStr += end_edit_section()
# Video section
peertubeStr = begin_edit_section(translate['Video Settings'])
peertube_instancesStr = ''
for url in peertube_instances:
peertube_instancesStr += url + '\n'
peertubeStr += \
edit_text_area(translate['Peertube Instances'], 'ptInstances',
peertube_instancesStr, 200, '', False)
peertubeStr += \
' <br>\n'
yt_replace_domain = get_config_param(base_dir, "youtubedomain")
if not yt_replace_domain:
yt_replace_domain = ''
peertubeStr += \
edit_text_field(translate['YouTube Replacement Domain'],
'ytdomain', yt_replace_domain)
peertubeStr += end_edit_section()
libretranslateUrl = get_config_param(base_dir, 'libretranslateUrl')
libretranslateApiKey = get_config_param(base_dir, 'libretranslateApiKey')
libretranslateStr = \
_html_edit_profile_libre_translate(translate,
libretranslateUrl,
libretranslateApiKey)
return instanceStr, roleAssignStr, peertubeStr, libretranslateStr
def _html_edit_profile_danger_zone(translate: {}) -> str:
"""danger zone section of Edit Profile screen
"""
editProfileForm = begin_edit_section(translate['Danger Zone'])
editProfileForm += \
' <b><label class="labels">' + \
translate['Danger Zone'] + '</label></b><br>\n'
editProfileForm += \
edit_check_box(translate['Deactivate this account'],
'deactivateThisAccount', False)
editProfileForm += end_edit_section()
return editProfileForm
def _html_system_monitor(nickname: str, translate: {}) -> str:
"""Links to performance graphs
"""
systemMonitorStr = begin_edit_section(translate['System Monitor'])
systemMonitorStr += '<p><a href="/users/' + nickname + \
'/performance?graph=get">📊 GET</a></p>'
systemMonitorStr += '<p><a href="/users/' + nickname + \
'/performance?graph=post">📊 POST</a></p>'
systemMonitorStr += end_edit_section()
return systemMonitorStr
def _html_edit_profile_skills(base_dir: str, nickname: str, domain: str,
translate: {}) -> str:
"""skills section of Edit Profile screen
"""
skills = get_skills(base_dir, nickname, domain)
skillsStr = ''
skillCtr = 1
if skills:
for skillDesc, skillValue in skills.items():
if is_filtered(base_dir, nickname, domain, skillDesc):
continue
skillsStr += \
'<p><input type="text" placeholder="' + translate['Skill'] + \
' ' + str(skillCtr) + '" name="skillName' + str(skillCtr) + \
'" value="' + skillDesc + '" style="width:40%">' + \
'<input type="range" min="1" max="100" ' + \
'class="slider" name="skillValue' + \
str(skillCtr) + '" value="' + str(skillValue) + '"></p>'
skillCtr += 1
skillsStr += \
'<p><input type="text" placeholder="Skill ' + str(skillCtr) + \
'" name="skillName' + str(skillCtr) + \
'" value="" style="width:40%">' + \
'<input type="range" min="1" max="100" ' + \
'class="slider" name="skillValue' + \
str(skillCtr) + '" 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.'
editProfileForm = \
begin_edit_section(translate['Skills']) + \
' <b><label class="labels">' + \
translate['Skills'] + '</label></b><br>\n' + \
' <label class="labels">' + \
translate[idx] + '</label>\n' + skillsStr
return editProfileForm
def _html_edit_profile_git_projects(base_dir: str, nickname: str, domain: str,
translate: {}) -> str:
"""git projects section of edit profile screen
"""
gitProjectsStr = ''
gitProjectsFilename = \
acct_dir(base_dir, nickname, domain) + '/gitprojects.txt'
if os.path.isfile(gitProjectsFilename):
with open(gitProjectsFilename, 'r') as gitProjectsFile:
gitProjectsStr = gitProjectsFile.read()
editProfileForm = begin_edit_section(translate['Git Projects'])
idx = 'List of project names that you wish to receive git patches for'
editProfileForm += \
edit_text_area(translate[idx], 'gitProjects', gitProjectsStr,
100, '', False)
editProfileForm += end_edit_section()
return editProfileForm
def _html_edit_profile_shared_items(base_dir: str, nickname: str, domain: str,
translate: {}) -> str:
"""shared items section of edit profile screen
"""
sharedItemsStr = ''
shared_items_federated_domainsStr = \
get_config_param(base_dir, 'shared_items_federated_domains')
if shared_items_federated_domainsStr:
shared_items_federated_domainsList = \
shared_items_federated_domainsStr.split(',')
for sharedFederatedDomain in shared_items_federated_domainsList:
sharedItemsStr += sharedFederatedDomain.strip() + '\n'
editProfileForm = begin_edit_section(translate['Shares'])
idx = 'List of domains which can access the shared items catalog'
editProfileForm += \
edit_text_area(translate[idx], 'shareDomainList',
sharedItemsStr, 200, '', False)
editProfileForm += end_edit_section()
return editProfileForm
def _html_edit_profile_filtering(base_dir: str, nickname: str, domain: str,
user_agents_blocked: str,
translate: {}, replyIntervalHours: int,
cw_lists: {}, lists_enabled: str) -> str:
"""Filtering and blocking section of edit profile screen
"""
filterStr = ''
filterFilename = \
acct_dir(base_dir, nickname, domain) + '/filters.txt'
if os.path.isfile(filterFilename):
with open(filterFilename, 'r') as filterfile:
filterStr = filterfile.read()
filterBioStr = ''
filterBioFilename = \
acct_dir(base_dir, nickname, domain) + '/filters_bio.txt'
if os.path.isfile(filterBioFilename):
with open(filterBioFilename, 'r') as filterfile:
filterBioStr = filterfile.read()
switchStr = ''
switchFilename = \
acct_dir(base_dir, nickname, domain) + '/replacewords.txt'
if os.path.isfile(switchFilename):
with open(switchFilename, 'r') as switchfile:
switchStr = switchfile.read()
autoTags = ''
autoTagsFilename = \
acct_dir(base_dir, nickname, domain) + '/autotags.txt'
if os.path.isfile(autoTagsFilename):
with open(autoTagsFilename, 'r') as autoTagsFile:
autoTags = autoTagsFile.read()
autoCW = ''
autoCWFilename = \
acct_dir(base_dir, nickname, domain) + '/autocw.txt'
if os.path.isfile(autoCWFilename):
with open(autoCWFilename, 'r') as autoCWFile:
autoCW = autoCWFile.read()
blockedStr = ''
blockedFilename = \
acct_dir(base_dir, nickname, domain) + '/blocking.txt'
if os.path.isfile(blockedFilename):
with open(blockedFilename, 'r') as blockedfile:
blockedStr = blockedfile.read()
dmAllowedInstancesStr = ''
dmAllowedInstancesFilename = \
acct_dir(base_dir, nickname, domain) + '/dmAllowedInstances.txt'
if os.path.isfile(dmAllowedInstancesFilename):
with open(dmAllowedInstancesFilename, 'r') as dmAllowedInstancesFile:
dmAllowedInstancesStr = dmAllowedInstancesFile.read()
allowedInstancesStr = ''
allowedInstancesFilename = \
acct_dir(base_dir, nickname, domain) + '/allowedinstances.txt'
if os.path.isfile(allowedInstancesFilename):
with open(allowedInstancesFilename, 'r') as allowedInstancesFile:
allowedInstancesStr = allowedInstancesFile.read()
editProfileForm = begin_edit_section(translate['Filtering and Blocking'])
idx = 'Hours after posting during which replies are allowed'
editProfileForm += \
' <label class="labels">' + \
translate[idx] + \
':</label> <input type="number" name="replyhours" ' + \
'min="0" max="999999999999" step="1" ' + \
'value="' + str(replyIntervalHours) + '"><br>\n'
editProfileForm += \
'<label class="labels">' + \
translate['City for spoofed GPS image metadata'] + \
'</label><br>\n'
city = ''
cityFilename = acct_dir(base_dir, nickname, domain) + '/city.txt'
if os.path.isfile(cityFilename):
with open(cityFilename, 'r') as fp:
city = fp.read().replace('\n', '')
locationsFilename = base_dir + '/custom_locations.txt'
if not os.path.isfile(locationsFilename):
locationsFilename = base_dir + '/locations.txt'
cities = []
with open(locationsFilename, 'r') as f:
cities = f.readlines()
cities.sort()
editProfileForm += ' <select id="cityDropdown" ' + \
'name="cityDropdown" class="theme">\n'
city = city.lower()
for cityName in cities:
if ':' not in cityName:
continue
citySelected = ''
cityName = cityName.split(':')[0]
cityName = cityName.lower()
if city:
if city in cityName:
citySelected = ' selected'
editProfileForm += \
' <option value="' + cityName + \
'"' + citySelected.title() + '>' + \
cityName + '</option>\n'
editProfileForm += ' </select><br>\n'
editProfileForm += \
' <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">' + \
filterStr + '</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">' + \
filterBioStr + '</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="switch_words" ' + \
'style="height:200px" spellcheck="false">' + \
switchStr + '</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">' + \
autoTags + '</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">' + autoCW + '</textarea>\n'
idx = 'Blocked accounts, one per line, in the form ' + \
'nickname@domain or *@blockeddomain'
editProfileForm += \
edit_text_area(translate['Blocked accounts'], 'blocked', blockedStr,
200, '', False)
idx = 'Direct messages are always allowed from these instances.'
editProfileForm += \
edit_text_area(translate['Direct Message permitted instances'],
'dmAllowedInstances', dmAllowedInstancesStr,
200, '', False)
idx = 'Federate only with a defined set of instances. ' + \
'One domain name per line.'
editProfileForm += \
' <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">' + \
allowedInstancesStr + '</textarea>\n'
if is_moderator(base_dir, nickname):
editProfileForm += \
'<a href="/users/' + nickname + '/crawlers">' + \
translate['Known Web Crawlers'] + '</a><br>\n'
user_agents_blockedStr = ''
for ua in user_agents_blocked:
if user_agents_blockedStr:
user_agents_blockedStr += '\n'
user_agents_blockedStr += ua
editProfileForm += \
edit_text_area(translate['Blocked User Agents'],
'user_agents_blockedStr', user_agents_blockedStr,
200, '', False)
cw_listsStr = ''
for name, item in cw_lists.items():
variableName = get_cw_list_variable(name)
listIsEnabled = False
if lists_enabled:
if name in lists_enabled:
listIsEnabled = True
if translate.get(name):
name = translate[name]
cw_listsStr += edit_check_box(name, variableName, listIsEnabled)
if cw_listsStr:
idx = 'Add content warnings for the following sites'
editProfileForm += \
'<label class="labels">' + translate[idx] + ':</label>\n' + \
'<br>' + cw_listsStr
editProfileForm += end_edit_section()
return editProfileForm
def _html_edit_profile_change_password(translate: {}) -> str:
"""Change password section of edit profile screen
"""
editProfileForm = \
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 editProfileForm
def _html_edit_profile_libre_translate(translate: {},
libretranslateUrl: str,
libretranslateApiKey: str) -> str:
"""Change automatic translation settings
"""
editProfileForm = begin_edit_section('LibreTranslate')
editProfileForm += \
edit_text_field('URL', 'libretranslateUrl', libretranslateUrl,
'http://0.0.0.0:5000')
editProfileForm += \
edit_text_field('API Key', 'libretranslateApiKey',
libretranslateApiKey)
editProfileForm += end_edit_section()
return editProfileForm
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.'
editProfileForm = \
begin_edit_section(translate['Background Images']) + \
' <label class="labels">' + translate[idx] + '</label><br><br>\n'
if not news_instance:
imageFormats = get_image_formats()
editProfileForm += \
' <label class="labels">' + \
translate['Background image'] + '</label>\n' + \
' <input type="file" id="image" name="image"' + \
' accept="' + imageFormats + '">\n' + \
' <br><label class="labels">' + \
translate['Timeline banner image'] + '</label>\n' + \
' <input type="file" id="banner" name="banner"' + \
' accept="' + imageFormats + '">\n' + \
' <br><label class="labels">' + \
translate['Search banner image'] + '</label>\n' + \
' <input type="file" id="search_banner" ' + \
'name="search_banner"' + \
' accept="' + imageFormats + '">\n' + \
' <br><label class="labels">' + \
translate['Left column image'] + '</label>\n' + \
' <input type="file" id="left_col_image" ' + \
'name="left_col_image"' + \
' accept="' + imageFormats + '">\n' + \
' <br><label class="labels">' + \
translate['Right column image'] + '</label>\n' + \
' <input type="file" id="right_col_image" ' + \
'name="right_col_image"' + \
' accept="' + imageFormats + '">\n'
editProfileForm += end_edit_section()
return editProfileForm
def _html_edit_profile_contact_info(nickname: str,
emailAddress: str,
xmppAddress: str,
matrixAddress: str,
ssbAddress: str,
toxAddress: str,
briarAddress: str,
jamiAddress: str,
cwtchAddress: str,
translate: {}) -> str:
"""Contact Information section of edit profile screen
"""
editProfileForm = begin_edit_section(translate['Contact Details'])
editProfileForm += edit_text_field(translate['Email'],
'email', emailAddress)
editProfileForm += edit_text_field(translate['XMPP'],
'xmppAddress', xmppAddress)
editProfileForm += edit_text_field(translate['Matrix'],
'matrixAddress', matrixAddress)
editProfileForm += edit_text_field('SSB', 'ssbAddress', ssbAddress)
editProfileForm += edit_text_field('Tox', 'toxAddress', toxAddress)
editProfileForm += edit_text_field('Briar', 'briarAddress', briarAddress)
editProfileForm += edit_text_field('Jami', 'jamiAddress', jamiAddress)
editProfileForm += edit_text_field('Cwtch', 'cwtchAddress', cwtchAddress)
editProfileForm += \
'<a href="/users/' + nickname + \
'/followingaccounts"><label class="labels">' + \
translate['Following'] + '</label></a><br>\n'
editProfileForm += end_edit_section()
return editProfileForm
def _html_edit_profile_encryption_keys(PGPfingerprint: str,
PGPpubKey: str,
EnigmaPubKey: str,
translate: {}) -> str:
"""Contact Information section of edit profile screen
"""
editProfileForm = begin_edit_section(translate['Encryption Keys'])
enigmaUrl = 'https://github.com/enigma-reloaded/enigma-reloaded'
editProfileForm += \
edit_text_field('<a href="' + enigmaUrl + '">Enigma</a>',
'enigmapubkey', EnigmaPubKey)
editProfileForm += edit_text_field(translate['PGP Fingerprint'],
'openpgp', PGPfingerprint)
editProfileForm += \
edit_text_area(translate['PGP'], 'pgp', PGPpubKey, 600,
'-----BEGIN PGP PUBLIC KEY BLOCK-----', False)
editProfileForm += end_edit_section()
return editProfileForm
def _html_edit_profile_options(isAdmin: bool,
manuallyApprovesFollowers: str,
isBot: str, isGroup: str,
followDMs: str, removeTwitter: str,
notifyLikes: str, notifyReactions: str,
hideLikeButton: str,
hideReactionButton: str,
translate: {}) -> str:
"""option checkboxes section of edit profile screen
"""
editProfileForm = ' <div class="container">\n'
editProfileForm += \
edit_check_box(translate['Approve follower requests'],
'approveFollowers', manuallyApprovesFollowers)
editProfileForm += \
edit_check_box(translate['This is a bot account'],
'isBot', isBot)
if isAdmin:
editProfileForm += \
edit_check_box(translate['This is a group account'],
'isGroup', isGroup)
editProfileForm += \
edit_check_box(translate['Only people I follow can send me DMs'],
'followDMs', followDMs)
editProfileForm += \
edit_check_box(translate['Remove Twitter posts'],
'removeTwitter', removeTwitter)
editProfileForm += \
edit_check_box(translate['Notify when posts are liked'],
'notifyLikes', notifyLikes)
editProfileForm += \
edit_check_box(translate['Notify on emoji reactions'],
'notifyReactions', notifyReactions)
editProfileForm += \
edit_check_box(translate["Don't show the Like button"],
'hideLikeButton', hideLikeButton)
editProfileForm += \
edit_check_box(translate["Don't show the Reaction button"],
'hideReactionButton', hideReactionButton)
editProfileForm += ' </div>\n'
return editProfileForm
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()
languagesStr = ''
for lang in lang_list:
if languagesStr:
languagesStr += ' / ' + lang
else:
languagesStr = lang
return languagesStr
def _html_edit_profile_main(base_dir: str, displayNickname: str, bioStr: str,
movedTo: str, donateUrl: str, websiteUrl: str,
blogAddress: str, actor_json: {},
translate: {}) -> str:
"""main info on edit profile screen
"""
imageFormats = get_image_formats()
editProfileForm = ' <div class="container">\n'
editProfileForm += \
edit_text_field(translate['Nickname'], 'displayNickname',
displayNickname)
editProfileForm += \
edit_text_area(translate['Your bio'], 'bio', bioStr, 200, '', True)
editProfileForm += \
' <label class="labels">' + translate['Avatar image'] + \
'</label>\n' + \
' <input type="file" id="avatar" name="avatar"' + \
' accept="' + imageFormats + '">\n'
occupationName = ''
if actor_json.get('hasOccupation'):
occupationName = get_occupation_name(actor_json)
editProfileForm += \
edit_text_field(translate['Occupation'], 'occupationName',
occupationName)
alsoKnownAsStr = ''
if actor_json.get('alsoKnownAs'):
alsoKnownAs = actor_json['alsoKnownAs']
ctr = 0
for altActor in alsoKnownAs:
if ctr > 0:
alsoKnownAsStr += ', '
ctr += 1
alsoKnownAsStr += altActor
editProfileForm += \
edit_text_field(translate['Other accounts'], 'alsoKnownAs',
alsoKnownAsStr, 'https://...')
editProfileForm += \
edit_text_field(translate['Moved to new account address'], 'movedTo',
movedTo, 'https://...')
editProfileForm += \
edit_text_field(translate['Donations link'], 'donateUrl',
donateUrl, 'https://...')
editProfileForm += \
edit_text_field(translate['Website'], 'websiteUrl',
websiteUrl, 'https://...')
editProfileForm += \
edit_text_field('Blog', 'blogAddress', blogAddress, 'https://...')
languagesListStr = _get_supported_languagesSorted(base_dir)
showLanguages = get_actor_languages(actor_json)
editProfileForm += \
edit_text_field(translate['Languages'], 'showLanguages',
showLanguages, languagesListStr)
editProfileForm += ' </div>\n'
return editProfileForm
def _html_edit_profile_top_banner(base_dir: str,
nickname: str, domain: str, domain_full: str,
defaultTimeline: str, bannerFile: str,
path: str, accessKeys: {},
translate: {}) -> str:
"""top banner on edit profile screen
"""
editProfileForm = \
'<a href="/users/' + nickname + '/' + defaultTimeline + '">' + \
'<img loading="lazy" class="timeline-banner" src="' + \
'/users/' + nickname + '/' + bannerFile + '" alt="" /></a>\n'
editProfileForm += \
'<form enctype="multipart/form-data" method="POST" ' + \
'accept-charset="UTF-8" action="' + path + '/profiledata">\n'
editProfileForm += ' <div class="vertical-center">\n'
editProfileForm += \
' <h1>' + translate['Profile for'] + \
' ' + nickname + '@' + domain_full + '</h1>'
editProfileForm += ' <div class="container">\n'
editProfileForm += \
' <center>\n' + \
' <input type="submit" name="submitProfile" ' + \
'accesskey="' + accessKeys['submitButton'] + '" ' + \
'value="' + translate['Submit'] + '">\n' + \
' </center>\n'
editProfileForm += ' </div>\n'
if scheduled_posts_exist(base_dir, nickname, domain):
editProfileForm += ' <div class="container">\n'
editProfileForm += \
edit_check_box(translate['Remove scheduled posts'],
'remove_scheduled_posts', False)
editProfileForm += ' </div>\n'
return editProfileForm
def html_edit_profile(css_cache: {}, translate: {}, base_dir: str, path: str,
domain: str, port: int, http_prefix: str,
defaultTimeline: str, theme: str,
peertube_instances: [],
text_mode_banner: str, city: str,
user_agents_blocked: str,
accessKeys: {},
default_reply_interval_hrs: int,
cw_lists: {}, lists_enabled: str) -> 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)
actorFilename = acct_dir(base_dir, nickname, domain) + '.json'
if not os.path.isfile(actorFilename):
return ''
# filename of the banner shown at the top
bannerFile, bannerFilename = \
get_banner_file(base_dir, nickname, domain, theme)
displayNickname = nickname
isBot = isGroup = followDMs = removeTwitter = ''
notifyLikes = notifyReactions = ''
hideLikeButton = hideReactionButton = media_instanceStr = ''
blogs_instanceStr = news_instanceStr = movedTo = twitterStr = ''
bioStr = donateUrl = websiteUrl = emailAddress = ''
PGPpubKey = EnigmaPubKey = ''
PGPfingerprint = xmppAddress = matrixAddress = ''
ssbAddress = blogAddress = toxAddress = jamiAddress = ''
cwtchAddress = briarAddress = manuallyApprovesFollowers = ''
actor_json = load_json(actorFilename)
if actor_json:
if actor_json.get('movedTo'):
movedTo = actor_json['movedTo']
donateUrl = get_donation_url(actor_json)
websiteUrl = get_website(actor_json, translate)
xmppAddress = get_xmpp_address(actor_json)
matrixAddress = get_matrix_address(actor_json)
ssbAddress = get_ssb_address(actor_json)
blogAddress = get_blog_address(actor_json)
toxAddress = get_tox_address(actor_json)
briarAddress = get_briar_address(actor_json)
jamiAddress = get_jami_address(actor_json)
cwtchAddress = get_cwtch_address(actor_json)
emailAddress = get_email_address(actor_json)
EnigmaPubKey = get_enigma_pub_key(actor_json)
PGPpubKey = get_pgp_pub_key(actor_json)
PGPfingerprint = get_pgp_fingerprint(actor_json)
if actor_json.get('name'):
if not is_filtered(base_dir, nickname, domain, actor_json['name']):
displayNickname = actor_json['name']
if actor_json.get('summary'):
bioStr = \
actor_json['summary'].replace('<p>', '').replace('</p>', '')
if is_filtered(base_dir, nickname, domain, bioStr):
bioStr = ''
if actor_json.get('manuallyApprovesFollowers'):
if actor_json['manuallyApprovesFollowers']:
manuallyApprovesFollowers = 'checked'
else:
manuallyApprovesFollowers = ''
if actor_json.get('type'):
if actor_json['type'] == 'Service':
isBot = 'checked'
isGroup = ''
elif actor_json['type'] == 'Group':
isGroup = 'checked'
isBot = ''
accountDir = acct_dir(base_dir, nickname, domain)
if os.path.isfile(accountDir + '/.followDMs'):
followDMs = 'checked'
if os.path.isfile(accountDir + '/.removeTwitter'):
removeTwitter = 'checked'
if os.path.isfile(accountDir + '/.notifyLikes'):
notifyLikes = 'checked'
if os.path.isfile(accountDir + '/.notifyReactions'):
notifyReactions = 'checked'
if os.path.isfile(accountDir + '/.hideLikeButton'):
hideLikeButton = 'checked'
if os.path.isfile(accountDir + '/.hideReactionButton'):
hideReactionButton = 'checked'
media_instance = get_config_param(base_dir, "media_instance")
if media_instance:
if media_instance is True:
media_instanceStr = 'checked'
blogs_instanceStr = news_instanceStr = ''
news_instance = get_config_param(base_dir, "news_instance")
if news_instance:
if news_instance is True:
news_instanceStr = 'checked'
blogs_instanceStr = media_instanceStr = ''
blogs_instance = get_config_param(base_dir, "blogs_instance")
if blogs_instance:
if blogs_instance is True:
blogs_instanceStr = 'checked'
media_instanceStr = news_instanceStr = ''
cssFilename = base_dir + '/epicyon-profile.css'
if os.path.isfile(base_dir + '/epicyon.css'):
cssFilename = base_dir + '/epicyon.css'
instanceStr = ''
roleAssignStr = ''
peertubeStr = ''
libretranslateStr = ''
systemMonitorStr = ''
graphicsStr = ''
sharesFederationStr = ''
adminNickname = get_config_param(base_dir, 'admin')
if is_artist(base_dir, nickname) or \
path.startswith('/users/' + str(adminNickname) + '/'):
graphicsStr = _html_edit_profile_graphic_design(base_dir, translate)
isAdmin = False
if adminNickname:
if path.startswith('/users/' + adminNickname + '/'):
isAdmin = True
twitterStr = \
_html_edit_profile_twitter(base_dir, translate, removeTwitter)
# shared items section
sharesFederationStr = \
_html_edit_profile_shared_items(base_dir, nickname,
domain, translate)
instanceStr, roleAssignStr, peertubeStr, libretranslateStr = \
_html_edit_profile_instance(base_dir, translate,
peertube_instances,
media_instanceStr,
blogs_instanceStr,
news_instanceStr)
systemMonitorStr = _html_system_monitor(nickname, translate)
instanceTitle = get_config_param(base_dir, 'instanceTitle')
editProfileForm = \
html_header_with_external_style(cssFilename, instanceTitle, None)
# keyboard navigation
userPathStr = '/users/' + nickname
userTimalineStr = '/users/' + nickname + '/' + defaultTimeline
menuTimeline = \
html_hide_from_screen_reader('🏠') + ' ' + \
translate['Switch to timeline view']
menuProfile = \
html_hide_from_screen_reader('👤') + ' ' + \
translate['Switch to profile view']
navLinks = {
menuProfile: userPathStr,
menuTimeline: userTimalineStr
}
navAccessKeys = {
menuProfile: 'p',
menuTimeline: 't'
}
editProfileForm += html_keyboard_navigation(text_mode_banner,
navLinks, navAccessKeys)
# top banner
editProfileForm += \
_html_edit_profile_top_banner(base_dir, nickname, domain, domain_full,
defaultTimeline, bannerFile,
path, accessKeys, translate)
# main info
editProfileForm += \
_html_edit_profile_main(base_dir, displayNickname, bioStr,
movedTo, donateUrl, websiteUrl,
blogAddress, actor_json, translate)
# Option checkboxes
editProfileForm += \
_html_edit_profile_options(isAdmin, manuallyApprovesFollowers,
isBot, isGroup, followDMs, removeTwitter,
notifyLikes, notifyReactions,
hideLikeButton, hideReactionButton,
translate)
# Contact information
editProfileForm += \
_html_edit_profile_contact_info(nickname, emailAddress,
xmppAddress, matrixAddress,
ssbAddress, toxAddress,
briarAddress, jamiAddress,
cwtchAddress, translate)
# Encryption Keys
editProfileForm += \
_html_edit_profile_encryption_keys(PGPfingerprint,
PGPpubKey, EnigmaPubKey, translate)
# Customize images and banners
editProfileForm += _html_edit_profile_background(news_instance, translate)
# Change password
editProfileForm += _html_edit_profile_change_password(translate)
# automatic translations
editProfileForm += libretranslateStr
# system monitor
editProfileForm += systemMonitorStr
# Filtering and blocking section
replyIntervalHours = get_reply_interval_hours(base_dir, nickname, domain,
default_reply_interval_hrs)
editProfileForm += \
_html_edit_profile_filtering(base_dir, nickname, domain,
user_agents_blocked, translate,
replyIntervalHours,
cw_lists, lists_enabled)
# git projects section
editProfileForm += \
_html_edit_profile_git_projects(base_dir, nickname, domain, translate)
# Skills section
editProfileForm += \
_html_edit_profile_skills(base_dir, nickname, domain, translate)
editProfileForm += roleAssignStr + peertubeStr + graphicsStr
editProfileForm += sharesFederationStr + twitterStr + instanceStr
# danger zone section
editProfileForm += _html_edit_profile_danger_zone(translate)
editProfileForm += ' <div class="container">\n'
editProfileForm += \
' <center>\n' + \
' <input type="submit" name="submitProfile" value="' + \
translate['Submit'] + '">\n' + \
' </center>\n'
editProfileForm += ' </div>\n'
editProfileForm += ' </div>\n'
editProfileForm += '</form>\n'
editProfileForm += html_footer()
return editProfileForm
def _individual_follow_as_html(signing_priv_key_pem: str,
translate: {},
base_dir: str, session,
cached_webfingers: {},
person_cache: {}, domain: str,
followUrl: str,
authorized: bool,
actorNickname: str,
http_prefix: str,
project_version: str,
dormant: bool,
debug: bool,
buttons=[]) -> str:
"""An individual follow entry on the profile screen
"""
followUrlNickname = get_nickname_from_actor(followUrl)
followUrlDomain, followUrlPort = get_domain_from_actor(followUrl)
followUrlDomainFull = get_full_domain(followUrlDomain, followUrlPort)
titleStr = '@' + followUrlNickname + '@' + followUrlDomainFull
avatarUrl = get_person_avatar_url(base_dir, followUrl, person_cache, True)
if not avatarUrl:
avatarUrl = followUrl + '/avatar.png'
displayName = get_display_name(base_dir, followUrl, person_cache)
isGroup = False
if not displayName:
# lookup the correct webfinger for the followUrl
followUrlHandle = followUrlNickname + '@' + followUrlDomainFull
followUrlWf = \
webfinger_handle(session, followUrlHandle, http_prefix,
cached_webfingers,
domain, __version__, debug, False,
signing_priv_key_pem)
originDomain = domain
(inboxUrl, pubKeyId, pubKey, fromPersonId, sharedInbox, avatarUrl2,
displayName, isGroup) = get_person_box(signing_priv_key_pem,
originDomain,
base_dir, session,
followUrlWf,
person_cache, project_version,
http_prefix, followUrlNickname,
domain, 'outbox', 43036)
if avatarUrl2:
avatarUrl = avatarUrl2
if displayName:
displayName = \
add_emoji_to_display_name(None, base_dir, http_prefix,
actorNickname, domain,
displayName, False)
titleStr = displayName
if dormant:
titleStr += ' 💤'
buttonsStr = ''
if authorized:
for b in buttons:
if b == 'block':
buttonsStr += \
'<a href="/users/' + actorNickname + \
'?options=' + followUrl + \
';1;' + avatarUrl + '"><button class="buttonunfollow">' + \
translate['Block'] + '</button></a>\n'
elif b == 'unfollow':
unfollowStr = 'Unfollow'
if isGroup or \
is_group_account(base_dir,
followUrlNickname, followUrlDomain):
unfollowStr = 'Leave'
buttonsStr += \
'<a href="/users/' + actorNickname + \
'?options=' + followUrl + \
';1;' + avatarUrl + '"><button class="buttonunfollow">' + \
translate[unfollowStr] + '</button></a>\n'
resultStr = '<div class="container">\n'
resultStr += \
'<a href="/users/' + actorNickname + '?options=' + \
followUrl + ';1;' + avatarUrl + '">\n'
resultStr += '<p><img loading="lazy" src="' + avatarUrl + '" alt=" ">'
resultStr += titleStr + '</a>' + buttonsStr + '</p>\n'
resultStr += '</div>\n'
return resultStr