__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