__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 getDisplayName 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 getNicknameFromActor from utils import getDomainFromActor 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 getReplyIntervalHours from languages import getActorLanguages from skills import getSkills from theme import getThemesList from person import personBoxJson from person import getActorJson from person import getPersonAvatarUrl from posts import getPersonBox from posts import isModerator from posts import parseUserFeed from posts import isCreateInsideAnnounce from donate import getDonationUrl from donate import getWebsite from xmpp import getXmppAddress from matrix import getMatrixAddress from ssb import getSSBAddress from pgp import getEmailAddress from pgp import getPGPfingerprint from pgp import getPGPpubKey from enigma import getEnigmaPubKey from tox import getToxAddress from briar import getBriarAddress from jami import getJamiAddress from cwtch import getCwtchAddress 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 getBlogAddress from webapp_post import individualPostAsHtml from webapp_timeline import htmlIndividualShare from blocking import getCWlistVariable 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(cssCache: {}, 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 = getDomainFromActor(personUrl) if not searchDomain: return None searchNickname = getNicknameFromActor(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 = getNicknameFromActor(movedTo) newDomain, newPort = getDomainFromActor(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 = getDomainFromActor(altActor) otherAccountsHtml += \ '' + altDomain + '' elif isinstance(alsoKnownAs, str): if alsoKnownAs != actor: ctr += 1 altDomain, altPort = getDomainFromActor(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 = getNicknameFromActor(movedTo) newDomain, newPort = getDomainFromActor(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 = getDomainFromActor(altActor) otherAccountshtml += \ '' + altDomain + '' elif isinstance(alsoKnownAs, str): if alsoKnownAs != actor: ctr += 1 altDomain, altPort = getDomainFromActor(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, cssCache: {}, 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, cssCache, 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 = getDomainFromActor(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 = getDonationUrl(profile_json) websiteUrl = getWebsite(profile_json, translate) blogAddress = getBlogAddress(profile_json) EnigmaPubKey = getEnigmaPubKey(profile_json) PGPpubKey = getPGPpubKey(profile_json) PGPfingerprint = getPGPfingerprint(profile_json) emailAddress = getEmailAddress(profile_json) xmppAddress = getXmppAddress(profile_json) matrixAddress = getMatrixAddress(profile_json) ssbAddress = getSSBAddress(profile_json) toxAddress = getToxAddress(profile_json) briarAddress = getBriarAddress(profile_json) jamiAddress = getJamiAddress(profile_json) cwtchAddress = getCwtchAddress(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, _ = \ getDomainFromActor(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 = \ personBoxJson({}, 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('