__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 webfingerHandle 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 getActorLanguages from skills import getSkills from theme import getThemesList from person import person_box_json from person import getActorJson from person import getPersonAvatarUrl from posts import getPersonBox from posts import is_moderator from posts import parseUserFeed from posts import isCreateInsideAnnounce 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 isFiltered from follow import isFollowerOfPerson from follow import getFollowerDomains from webapp_frontscreen import htmlFrontScreen from webapp_utils import htmlKeyboardNavigation from webapp_utils import htmlHideFromScreenReader from webapp_utils import scheduledPostsExist from webapp_utils import htmlHeaderWithExternalStyle from webapp_utils import htmlHeaderWithPersonMarkup from webapp_utils import htmlFooter from webapp_utils import addEmojiToDisplayName from webapp_utils import getBannerFile from webapp_utils import htmlPostSeparator from webapp_utils import editCheckBox from webapp_utils import editTextField from webapp_utils import editTextArea from webapp_utils import beginEditSection from webapp_utils import endEditSection from blog import get_blog_address from webapp_post import individualPostAsHtml from webapp_timeline import htmlIndividualShare from blocking import get_cw_list_variable def _validProfilePreviewPost(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 isCreateInsideAnnounce(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 htmlProfileAfterSearch(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 = \ getActorJson(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 = getPersonAvatarUrl(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 = \ isFollowerOfPerson(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 '
' in profileDescription: if len(profileDescription.split('
')) > 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('
', '\n') avatarDescription = avatarDescription.replace('

', '') avatarDescription = avatarDescription.replace('

', '') 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 = \ _getProfileHeaderAfterSearch(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 += \ '
\n' + \ '
\n' + \ '
\n' + \ ' \n' + \ ' \n' + \ ' \n' + \ '
\n' + \ '
\n' + \ '
\n' else: profileStr += \ '
\n' + \ '
\n' + \ '
\n' + \ ' \n' + \ ' \n' + \ '
\n' + \ '
\n' + \ '
\n' userFeed = \ parseUserFeed(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 = \ _validProfilePreviewPost(item, personUrl) if not showItem: continue profileStr += \ individualPostAsHtml(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 htmlHeaderWithExternalStyle(cssFilename, instanceTitle, None) + \ profileStr + htmlFooter() def _getProfileHeader(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
\n' + \ ' \n' + \ ' \n' + \ '
\n' + \ ' \n' + \ ' \n' occupationStr = '' if occupationName: occupationStr += \ ' ' + occupationName + '
\n' htmlStr += '

' + displayName + '

\n' + occupationStr htmlStr += \ '

@' + nickname + '@' + domain_full + '
\n' if joinedDate: htmlStr += \ '

' + translate['Joined'] + ' ' + \ joinedDate.split('T')[0] + '
\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 += \ '

' + translate['New account'] + ': ' + \ '@' + \ newNickname + '@' + newDomainFull + '
\n' elif alsoKnownAs: otherAccountsHtml = \ '

' + 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 += \ '' + altDomain + '' elif isinstance(alsoKnownAs, str): if alsoKnownAs != actor: ctr += 1 altDomain, altPort = get_domain_from_actor(alsoKnownAs) otherAccountsHtml += \ '' + altDomain + '' otherAccountsHtml += '

\n' if ctr > 0: htmlStr += otherAccountsHtml htmlStr += \ ' ' + \ '' + translate['QR Code'] + \
        '

\n' + \ '

' + profileDescriptionShort + '

\n' + loginButton if pinnedContent: htmlStr += pinnedContent.replace('

', '

📎', 1) htmlStr += \ '

\n' + \ '
\n\n' return htmlStr def _getProfileHeaderAfterSearch(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
\n' + \ ' \n' + \ ' \n' + \ '
\n' if avatarUrl: htmlStr += \ ' \n' + \ ' \n' if not displayName: displayName = searchNickname htmlStr += \ '

' + displayName + '

\n' + \ '

@' + searchNickname + '@' + searchDomainFull + '
\n' if joinedDate: htmlStr += '

' + translate['Joined'] + ' ' + \ joinedDate.split('T')[0] + '

\n' if followsYou: htmlStr += '

' + translate['Follows you'] + '

\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 += '

' + translate['New account'] + \ ': @' + newHandle + '

\n' elif alsoKnownAs: otherAccountshtml = \ '

' + 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 += \ '' + altDomain + '' elif isinstance(alsoKnownAs, str): if alsoKnownAs != actor: ctr += 1 altDomain, altPort = get_domain_from_actor(alsoKnownAs) otherAccountshtml += \ '' + altDomain + '' otherAccountshtml += '

\n' if ctr > 0: htmlStr += otherAccountshtml htmlStr += \ '

' + profileDescriptionShort + '

\n' + \ '
\n' + \ '
\n\n' return htmlStr def htmlProfile(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 htmlFrontScreen(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 = \ addEmojiToDisplayName(session, base_dir, http_prefix, nickname, domain, profile_json['name'], True) domain_full = get_full_domain(domain, port) profileDescription = \ addEmojiToDisplayName(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 = '
\n' donateSection += '
\n' if donateUrl and not is_system_account(nickname): donateSection += \ '

\n' if websiteUrl: donateSection += \ '

' + translate['Website'] + ': ' + websiteUrl + '

\n' if emailAddress: donateSection += \ '

' + translate['Email'] + ': ' + emailAddress + '

\n' if blogAddress: donateSection += \ '

Blog: ' + blogAddress + '

\n' if xmppAddress: donateSection += \ '

' + translate['XMPP'] + ': ' + xmppAddress + '

\n' if matrixAddress: donateSection += \ '

' + translate['Matrix'] + ': ' + matrixAddress + '

\n' if ssbAddress: donateSection += \ '

SSB:

\n' if toxAddress: donateSection += \ '

Tox:

\n' if briarAddress: if briarAddress.startswith('briar://'): donateSection += \ '

\n' else: donateSection += \ '

briar://

\n' if jamiAddress: donateSection += \ '

Jami:

\n' if cwtchAddress: donateSection += \ '

Cwtch:

\n' if EnigmaPubKey: donateSection += \ '

Enigma:

\n' if PGPfingerprint: donateSection += \ '

PGP: ' + \ PGPfingerprint.replace('\n', '
') + '

\n' if PGPpubKey: donateSection += \ '

' + PGPpubKey.replace('\n', '
') + '

\n' donateSection += '
\n' donateSection += '
\n' if authorized: editProfileStr = \ '' + \ '| ' + translate['Edit'] + '\n' logoutStr = \ '' + \ '| ' + translate['Logout'] + \
            '\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 = \ getFollowerDomains(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 += '
' followApprovalsSection += \ '' followApprovalsSection += \ '' + \ followerHandle + \ newFollowerDomain + '' # show Approve and Deny buttons followApprovalsSection += \ '' followApprovalsSection += \ '

' followApprovalsSection += \ '' followApprovalsSection += \ '' followApprovalsSection += '
' profileDescriptionShort = profileDescription if '\n' in profileDescription: if len(profileDescription.split('\n')) > 2: profileDescriptionShort = '' else: if '
' in profileDescription: if len(profileDescription.split('
')) > 2: profileDescriptionShort = '' profileDescription = profileDescription.replace('
', '\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('
', '\n') avatarDescription = avatarDescription.replace('

', '') avatarDescription = avatarDescription.replace('

', '') 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 = \ _getProfileHeader(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 = \ htmlHideFromScreenReader('🏠') + ' ' + \ translate['Switch to timeline view'] menuEdit = \ htmlHideFromScreenReader('✍') + ' ' + translate['Edit'] if not isGroup: menuFollowing = \ htmlHideFromScreenReader('👥') + ' ' + translate['Following'] menuFollowers = \ htmlHideFromScreenReader('👪') + ' ' + followersStr if not isGroup: menuRoles = \ htmlHideFromScreenReader('🤚') + ' ' + translate['Roles'] menuSkills = \ htmlHideFromScreenReader('🛠') + ' ' + translate['Skills'] menuLogout = \ htmlHideFromScreenReader('❎') + ' ' + 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 = \ htmlHideFromScreenReader('🎨') + ' ' + 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 = htmlKeyboardNavigation(text_mode_banner, navLinks, navAccessKeys) profileStr += profileHeaderStr + donateSection profileStr += '
\n' profileStr += '
' profileStr += \ ' ' if not isGroup: profileStr += \ ' ' + \ '' profileStr += \ ' ' + \ '' if not isGroup: profileStr += \ ' ' + \ '' profileStr += \ ' ' + \ '' # profileStr += \ # ' ' + \ # '' # profileStr += \ # ' ' + \ # '' profileStr += logoutStr + editProfileStr profileStr += '
' profileStr += '
' # start of #timeline profileStr += '
\n' profileStr += followApprovalsSection cssFilename = base_dir + '/epicyon-profile.css' if os.path.isfile(base_dir + '/epicyon.css'): cssFilename = base_dir + '/epicyon.css' licenseStr = \ '' + \ '' + \
        translate['Get the source code'] + '' if selected == 'posts': profileStr += \ _htmlProfilePosts(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 += \ _htmlProfileFollowing(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 += \ _htmlProfileFollowing(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 += \ _htmlProfileRoles(translate, nickname, domain_full, extraJson) elif selected == 'skills': profileStr += \ _htmlProfileSkills(translate, nickname, domain_full, extraJson) # elif selected == 'shares': # profileStr += \ # _htmlProfileShares(actor, translate, # nickname, domain_full, # extraJson, 'shares') + licenseStr # elif selected == 'wanted': # profileStr += \ # _htmlProfileShares(actor, translate, # nickname, domain_full, # extraJson, 'wanted') + licenseStr # end of #timeline profileStr += '
' instanceTitle = \ get_config_param(base_dir, 'instanceTitle') profileStr = \ htmlHeaderWithPersonMarkup(cssFilename, instanceTitle, profile_json, city, content_license_url) + \ profileStr + htmlFooter() return profileStr def _htmlProfilePosts(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 = htmlPostSeparator(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 = \ individualPostAsHtml(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 _htmlProfileFollowing(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 += \ '
\n' + \ ' ' + \
                translate['Page up'] + '\n' + \ '
\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 += \ _individualFollowAsHtml(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 += \ '
\n' + \ ' ' + \
                translate['Page down'] + '\n' + \ '
\n' return profileStr def _htmlProfileRoles(translate: {}, nickname: str, domain: str, rolesList: []) -> str: """Shows roles on the profile screen """ profileStr = '' profileStr += \ '
\n
\n' for role in rolesList: if translate.get(role): profileStr += '

' + translate[role] + '

\n' else: profileStr += '

' + role + '

\n' profileStr += '
\n' if len(profileStr) == 0: profileStr += \ '

@' + nickname + '@' + domain + ' has no roles assigned

\n' else: profileStr = '
' + profileStr + '
\n' return profileStr def _htmlProfileSkills(translate: {}, nickname: str, domain: str, skillsJson: {}) -> str: """Shows skills on the profile screen """ profileStr = '' for skill, level in skillsJson.items(): profileStr += \ '
' + skill + \ '
\n
\n' if len(profileStr) > 0: profileStr = '
' + \ profileStr + '
\n' return profileStr def _htmlProfileShares(actor: str, translate: {}, nickname: str, domain: str, sharesJson: {}, sharesFileType: str) -> str: """Shows shares on the profile screen """ profileStr = '' for item in sharesJson['orderedItems']: profileStr += htmlIndividualShare(domain, item['shareId'], actor, item, translate, False, False, sharesFileType) if len(profileStr) > 0: profileStr = '
' + profileStr + '
\n' return profileStr def _grayscaleEnabled(base_dir: str) -> bool: """Is grayscale UI enabled? """ return os.path.isfile(base_dir + '/accounts/.grayscale') def _htmlThemesDropdown(base_dir: str, translate: {}) -> str: """Returns the html for theme selection dropdown """ # Themes section themes = getThemesList(base_dir) themesDropdown = '
\n' grayscale = _grayscaleEnabled(base_dir) themesDropdown += \ editCheckBox(translate['Grayscale'], 'grayscale', grayscale) themesDropdown += '
' 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 += \ editCheckBox(translate['Remove the custom font'], 'removeCustomFont', False) theme_name = get_config_param(base_dir, 'theme') themesDropdown = \ themesDropdown.replace('