__filename__ = "webapp_profile.py" __author__ = "Bob Mottram" __license__ = "AGPL3+" __version__ = "1.2.0" __maintainer__ = "Bob Mottram" __email__ = "bob@freedombone.net" __status__ = "Production" import os from pprint import pprint from utils import getOccupationName from utils import getLockedAccount from utils import hasUsersPath from utils import getFullDomain from utils import isArtist from utils import isDormant from utils import getNicknameFromActor from utils import getDomainFromActor from utils import isSystemAccount from utils import removeHtml from utils import loadJson from utils import getConfigParam from utils import getImageFormats from skills import getSkills from theme import getThemesList from person import personBoxJson from webfinger import webfingerHandle from session import getJson from posts import parseUserFeed from posts import getUserUrl from posts import getPersonBox from donate import getDonationUrl 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 tox import getToxAddress from briar import getBriarAddress from jami import getJamiAddress from filters import isFiltered from follow import isFollowerOfPerson from webapp_frontscreen import htmlFrontScreen from webapp_utils import htmlKeyboardNavigation from webapp_utils import htmlHideFromScreenReader from webapp_utils import scheduledPostsExist from webapp_utils import getPersonAvatarUrl 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 getBlogAddress from webapp_post import individualPostAsHtml from webapp_timeline import htmlIndividualShare def htmlProfileAfterSearch(cssCache: {}, recentPostsCache: {}, maxRecentPosts: int, translate: {}, baseDir: str, path: str, httpPrefix: str, nickname: str, domain: str, port: int, profileHandle: str, session, cachedWebfingers: {}, personCache: {}, debug: bool, projectVersion: str, YTReplacementDomain: str, showPublishedDateOnly: bool, defaultTimeline: str, peertubeInstances: [], allowLocalNetworkAccess: bool, themeName: str, accessKeys: {}) -> str: """Show a profile page after a search for a fediverse address """ if hasUsersPath(profileHandle) or '/@' in profileHandle: searchNickname = getNicknameFromActor(profileHandle) searchDomain, searchPort = getDomainFromActor(profileHandle) else: if '@' not in profileHandle: if debug: print('DEBUG: no @ in ' + profileHandle) return None if profileHandle.startswith('@'): profileHandle = profileHandle[1:] if '@' not in profileHandle: if debug: print('DEBUG: no @ in ' + profileHandle) return None searchNickname = profileHandle.split('@')[0] searchDomain = profileHandle.split('@')[1] searchPort = None if ':' in searchDomain: searchPortStr = searchDomain.split(':')[1] if searchPortStr.isdigit(): searchPort = int(searchPortStr) searchDomain = searchDomain.split(':')[0] if searchPort: if debug: print('DEBUG: Search for handle ' + str(searchNickname) + '@' + str(searchDomain) + ':' + str(searchPort)) else: if debug: print('DEBUG: Search for handle ' + str(searchNickname) + '@' + str(searchDomain)) if not searchNickname: if debug: print('DEBUG: No nickname found in ' + profileHandle) return None if not searchDomain: if debug: print('DEBUG: No domain found in ' + profileHandle) return None searchDomainFull = getFullDomain(searchDomain, searchPort) profileStr = '' cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' wf = \ webfingerHandle(session, searchNickname + '@' + searchDomainFull, httpPrefix, cachedWebfingers, domain, projectVersion, debug) if not wf: if debug: print('DEBUG: Unable to webfinger ' + searchNickname + '@' + searchDomainFull) print('DEBUG: cachedWebfingers ' + str(cachedWebfingers)) print('DEBUG: httpPrefix ' + httpPrefix) print('DEBUG: domain ' + domain) return None if not isinstance(wf, dict): if debug: print('WARN: Webfinger search for ' + searchNickname + '@' + searchDomainFull + ' did not return a dict. ' + str(wf)) return None personUrl = None if wf.get('errors'): personUrl = httpPrefix + '://' + \ searchDomainFull + '/users/' + searchNickname profileStr = 'https://www.w3.org/ns/activitystreams' asHeader = { 'Accept': 'application/activity+json; profile="' + profileStr + '"' } if not personUrl: personUrl = getUserUrl(wf, 0, debug) if not personUrl: # try single user instance asHeader = { 'Accept': 'application/ld+json; profile="' + profileStr + '"' } personUrl = httpPrefix + '://' + searchDomainFull profileJson = \ getJson(session, personUrl, asHeader, None, debug, projectVersion, httpPrefix, domain) if not profileJson: asHeader = { 'Accept': 'application/ld+json; profile="' + profileStr + '"' } profileJson = \ getJson(session, personUrl, asHeader, None, debug, projectVersion, httpPrefix, domain) if not profileJson: print('DEBUG: No actor returned from ' + personUrl) return None avatarUrl = '' if profileJson.get('icon'): if profileJson['icon'].get('url'): avatarUrl = profileJson['icon']['url'] if not avatarUrl: avatarUrl = getPersonAvatarUrl(baseDir, personUrl, personCache, True) displayName = searchNickname if profileJson.get('name'): displayName = profileJson['name'] lockedAccount = getLockedAccount(profileJson) if lockedAccount: displayName += '🔒' movedTo = '' if profileJson.get('movedTo'): movedTo = profileJson['movedTo'] displayName += ' ⌂' followsYou = \ isFollowerOfPerson(baseDir, nickname, domain, searchNickname, searchDomainFull) profileDescription = '' if profileJson.get('summary'): profileDescription = profileJson['summary'] outboxUrl = None if not profileJson.get('outbox'): if debug: pprint(profileJson) print('DEBUG: No outbox found') return None outboxUrl = profileJson['outbox'] # profileBackgroundImage = '' # if profileJson.get('image'): # if profileJson['image'].get('url'): # profileBackgroundImage = profileJson['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 profileJson.get('summary'): if isinstance(profileJson['summary'], str): avatarDescription = \ profileJson['summary'].replace('
', '\n') avatarDescription = avatarDescription.replace('

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

', '') if '<' in avatarDescription: avatarDescription = removeHtml(avatarDescription) imageUrl = '' if profileJson.get('image'): if profileJson['image'].get('url'): imageUrl = profileJson['image']['url'] alsoKnownAs = None if profileJson.get('alsoKnownAs'): alsoKnownAs = profileJson['alsoKnownAs'] joinedDate = None if profileJson.get('published'): if 'T' in profileJson['published']: joinedDate = profileJson['published'] profileStr = \ _getProfileHeaderAfterSearch(baseDir, nickname, defaultTimeline, searchNickname, searchDomainFull, translate, displayName, followsYou, profileDescriptionShort, avatarUrl, imageUrl, movedTo, profileJson['id'], alsoKnownAs, accessKeys, joinedDate) domainFull = getFullDomain(domain, port) followIsPermitted = True if searchNickname == 'news' and searchDomainFull == domainFull: # currently the news actor is not something you can follow followIsPermitted = False elif searchNickname == nickname and searchDomainFull == domainFull: # don't follow yourself! followIsPermitted = False if followIsPermitted: profileStr += '
\n' profileStr += '
\n' profileStr += '
\n' profileStr += \ ' \n' profileStr += \ ' \n' profileStr += \ ' \n' profileStr += '
\n' profileStr += '
\n' profileStr += '
\n' i = 0 for item in parseUserFeed(session, outboxUrl, asHeader, projectVersion, httpPrefix, domain): if not item.get('actor'): continue if item['actor'] != personUrl: continue if not item.get('type'): continue if item['type'] != 'Create': continue if not item.get('object'): continue profileStr += \ individualPostAsHtml(True, recentPostsCache, maxRecentPosts, translate, None, baseDir, session, cachedWebfingers, personCache, nickname, domain, port, item, avatarUrl, False, False, httpPrefix, projectVersion, 'inbox', YTReplacementDomain, showPublishedDateOnly, peertubeInstances, allowLocalNetworkAccess, themeName, False, False, False, False, False) i += 1 if i >= 20: break instanceTitle = \ getConfigParam(baseDir, 'instanceTitle') return htmlHeaderWithExternalStyle(cssFilename, instanceTitle) + \ profileStr + htmlFooter() def _getProfileHeader(baseDir: str, httpPrefix: str, nickname: str, domain: str, domainFull: 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' htmlStr += ' \n' htmlStr += ' \n' htmlStr += '
\n' htmlStr += \ ' \n' + \ ' \n' occupationStr = '' if occupationName: occupationStr += \ ' ' + occupationName + '
\n' htmlStr += '

' + displayName + '

\n' + occupationStr htmlStr += \ '

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

' + translate['Joined'] + ' ' + \ joinedDate.split('T')[0] + '
\n' if movedTo: newNickname = getNicknameFromActor(movedTo) newDomain, newPort = getDomainFromActor(movedTo) newDomainFull = getFullDomain(newDomain, newPort) if newNickname and newDomain: htmlStr += \ '

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

' + translate['Other accounts'] + ': ' actor = httpPrefix + '://' + domainFull + '/users/' + nickname 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' htmlStr += '

' + profileDescriptionShort + '

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

', '

📎', 1) htmlStr += '

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

' + displayName + '

\n' htmlStr += \ '

@' + 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 = getFullDomain(newDomain, newPort) if newNickname and newDomain: newHandle = newNickname + '@' + newDomainFull htmlStr += '

' + translate['New account'] + \ ': < a href="' + movedTo + '">@' + 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' htmlStr += '
\n' htmlStr += '
\n\n' return htmlStr def htmlProfile(rssIconAtTop: bool, cssCache: {}, iconsAsButtons: bool, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, projectVersion: str, baseDir: str, httpPrefix: str, authorized: bool, profileJson: {}, selected: str, session, cachedWebfingers: {}, personCache: {}, YTReplacementDomain: str, showPublishedDateOnly: bool, newswire: {}, theme: str, dormantMonths: int, peertubeInstances: [], allowLocalNetworkAccess: bool, textModeBanner: str, debug: bool, accessKeys: {}, city: str, extraJson=None, pageNumber=None, maxItemsPerPage=None) -> str: """Show the profile page as html """ nickname = profileJson['preferredUsername'] if not nickname: return "" if isSystemAccount(nickname): return htmlFrontScreen(rssIconAtTop, cssCache, iconsAsButtons, defaultTimeline, recentPostsCache, maxRecentPosts, translate, projectVersion, baseDir, httpPrefix, authorized, profileJson, selected, session, cachedWebfingers, personCache, YTReplacementDomain, showPublishedDateOnly, newswire, theme, extraJson, allowLocalNetworkAccess, accessKeys, pageNumber, maxItemsPerPage) domain, port = getDomainFromActor(profileJson['id']) if not domain: return "" displayName = \ addEmojiToDisplayName(baseDir, httpPrefix, nickname, domain, profileJson['name'], True) domainFull = domain if port: domainFull = domain + ':' + str(port) profileDescription = \ addEmojiToDisplayName(baseDir, httpPrefix, nickname, domain, profileJson['summary'], False) postsButton = 'button' followingButton = 'button' followersButton = 'button' rolesButton = 'button' skillsButton = 'button' sharesButton = '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' loginButton = '' followApprovalsSection = '' followApprovals = False editProfileStr = '' logoutStr = '' actor = profileJson['id'] usersPath = '/users/' + actor.split('/users/')[1] donateSection = '' donateUrl = getDonationUrl(profileJson) PGPpubKey = getPGPpubKey(profileJson) PGPfingerprint = getPGPfingerprint(profileJson) emailAddress = getEmailAddress(profileJson) xmppAddress = getXmppAddress(profileJson) matrixAddress = getMatrixAddress(profileJson) ssbAddress = getSSBAddress(profileJson) toxAddress = getToxAddress(profileJson) briarAddress = getBriarAddress(profileJson) jamiAddress = getJamiAddress(profileJson) if donateUrl or xmppAddress or matrixAddress or \ ssbAddress or toxAddress or briarAddress or \ jamiAddress or PGPpubKey or \ PGPfingerprint or emailAddress: donateSection = '
\n' donateSection += '
\n' if donateUrl and not isSystemAccount(nickname): donateSection += \ '

\n' if emailAddress: donateSection += \ '

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

\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 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 = \ baseDir + '/accounts/' + \ 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: with open(followRequestsFilename, 'r') as f: for followerHandle in f: if len(line) > 0: if '://' in followerHandle: followerActor = followerHandle else: followerActor = \ httpPrefix + '://' + \ followerHandle.split('@')[1] + \ '/users/' + followerHandle.split('@')[0] basePath = '/users/' + nickname followApprovalsSection += '
' followApprovalsSection += \ '' followApprovalsSection += \ '' + \ followerHandle + '' 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 profileJson.get('summary'): avatarDescription = profileJson['summary'].replace('
', '\n') avatarDescription = avatarDescription.replace('

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

', '') movedTo = '' if profileJson.get('movedTo'): movedTo = profileJson['movedTo'] alsoKnownAs = None if profileJson.get('alsoKnownAs'): alsoKnownAs = profileJson['alsoKnownAs'] joinedDate = None if profileJson.get('published'): if 'T' in profileJson['published']: joinedDate = profileJson['published'] occupationName = None if profileJson.get('hasOccupation'): occupationName = getOccupationName(profileJson) avatarUrl = profileJson['icon']['url'] # get pinned post content accountDir = baseDir + '/accounts/' + nickname + '@' + domain pinnedFilename = accountDir + '/pinToProfile.txt' pinnedContent = None if os.path.isfile(pinnedFilename): with open(pinnedFilename, 'r') as pinFile: pinnedContent = pinFile.read() profileHeaderStr = \ _getProfileHeader(baseDir, httpPrefix, nickname, domain, domainFull, translate, defaultTimeline, displayName, avatarDescription, profileDescriptionShort, loginButton, avatarUrl, theme, movedTo, alsoKnownAs, pinnedContent, accessKeys, joinedDate, occupationName) # keyboard navigation userPathStr = '/users/' + nickname deft = defaultTimeline menuTimeline = \ htmlHideFromScreenReader('🏠') + ' ' + \ translate['Switch to timeline view'] menuEdit = \ htmlHideFromScreenReader('✍') + ' ' + translate['Edit'] menuFollowing = \ htmlHideFromScreenReader('👥') + ' ' + translate['Following'] menuFollowers = \ htmlHideFromScreenReader('👪') + ' ' + translate['Followers'] menuRoles = \ htmlHideFromScreenReader('🤚') + ' ' + translate['Roles'] menuSkills = \ htmlHideFromScreenReader('🛠') + ' ' + translate['Skills'] menuShares = \ htmlHideFromScreenReader('🤝') + ' ' + translate['Shares'] 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', menuShares: userPathStr + '/shares#timeline', menuLogout: '/logout' } navAccessKeys = {} for variableName, key in accessKeys.items(): if not locals().get(variableName): continue navAccessKeys[locals()[variableName]] = key profileStr = htmlKeyboardNavigation(textModeBanner, navLinks, navAccessKeys) profileStr += profileHeaderStr + donateSection profileStr += '
\n' profileStr += '
' profileStr += \ ' ' profileStr += \ ' ' + \ '' profileStr += \ ' ' + \ '' profileStr += \ ' ' + \ '' profileStr += \ ' ' + \ '' profileStr += \ ' ' + \ '' profileStr += logoutStr + editProfileStr profileStr += '
' profileStr += '
' # start of #timeline profileStr += '
\n' profileStr += followApprovalsSection cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' licenseStr = \ '' + \ '' + \
        translate['Get the source code'] + '' if selected == 'posts': profileStr += \ _htmlProfilePosts(recentPostsCache, maxRecentPosts, translate, baseDir, httpPrefix, authorized, nickname, domain, port, session, cachedWebfingers, personCache, projectVersion, YTReplacementDomain, showPublishedDateOnly, peertubeInstances, allowLocalNetworkAccess, theme) + licenseStr elif selected == 'following': profileStr += \ _htmlProfileFollowing(translate, baseDir, httpPrefix, authorized, nickname, domain, port, session, cachedWebfingers, personCache, extraJson, projectVersion, ["unfollow"], selected, usersPath, pageNumber, maxItemsPerPage, dormantMonths, debug) elif selected == 'followers': profileStr += \ _htmlProfileFollowing(translate, baseDir, httpPrefix, authorized, nickname, domain, port, session, cachedWebfingers, personCache, extraJson, projectVersion, ["block"], selected, usersPath, pageNumber, maxItemsPerPage, dormantMonths, debug) elif selected == 'roles': profileStr += \ _htmlProfileRoles(translate, nickname, domainFull, extraJson) elif selected == 'skills': profileStr += \ _htmlProfileSkills(translate, nickname, domainFull, extraJson) elif selected == 'shares': profileStr += \ _htmlProfileShares(actor, translate, nickname, domainFull, extraJson) + licenseStr # end of #timeline profileStr += '
' instanceTitle = \ getConfigParam(baseDir, 'instanceTitle') profileStr = \ htmlHeaderWithPersonMarkup(cssFilename, instanceTitle, profileJson, city) + \ profileStr + htmlFooter() return profileStr def _htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int, translate: {}, baseDir: str, httpPrefix: str, authorized: bool, nickname: str, domain: str, port: int, session, cachedWebfingers: {}, personCache: {}, projectVersion: str, YTReplacementDomain: str, showPublishedDateOnly: bool, peertubeInstances: [], allowLocalNetworkAccess: bool, themeName: str) -> str: """Shows posts on the profile screen These should only be public posts """ separatorStr = htmlPostSeparator(baseDir, 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, baseDir, domain, port, outboxFeedPathStr, httpPrefix, 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(True, recentPostsCache, maxRecentPosts, translate, None, baseDir, session, cachedWebfingers, personCache, nickname, domain, port, item, None, True, False, httpPrefix, projectVersion, 'inbox', YTReplacementDomain, showPublishedDateOnly, peertubeInstances, allowLocalNetworkAccess, themeName, False, False, False, True, False) if postStr: profileStr += postStr + separatorStr ctr += 1 if ctr >= maxItems: break currPage += 1 return profileStr def _htmlProfileFollowing(translate: {}, baseDir: str, httpPrefix: str, authorized: bool, nickname: str, domain: str, port: int, session, cachedWebfingers: {}, personCache: {}, followingJson: {}, projectVersion: str, buttons: [], feedName: str, actor: str, pageNumber: int, maxItemsPerPage: int, dormantMonths: int, debug: bool) -> 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 = \ isDormant(baseDir, nickname, domain, followingActor, dormantMonths) profileStr += \ _individualFollowAsHtml(translate, baseDir, session, cachedWebfingers, personCache, domain, followingActor, authorized, nickname, httpPrefix, projectVersion, 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: {}) -> str: """Shows shares on the profile screen """ profileStr = '' for item in sharesJson['orderedItems']: profileStr += htmlIndividualShare(actor, item, translate, False, False) if len(profileStr) > 0: profileStr = '
' + profileStr + '
\n' return profileStr def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str, domain: str, port: int, httpPrefix: str, defaultTimeline: str, theme: str, peertubeInstances: [], textModeBanner: str, city: str) -> str: """Shows the edit profile screen """ imageFormats = getImageFormats() themeFormats = '.zip, .gz' path = path.replace('/inbox', '').replace('/outbox', '') path = path.replace('/shares', '') nickname = getNicknameFromActor(path) if not nickname: return '' domainFull = getFullDomain(domain, port) actorFilename = \ baseDir + '/accounts/' + nickname + '@' + domain + '.json' if not os.path.isfile(actorFilename): return '' # filename of the banner shown at the top bannerFile, bannerFilename = \ getBannerFile(baseDir, nickname, domain, theme) isBot = '' isGroup = '' followDMs = '' removeTwitter = '' notifyLikes = '' hideLikeButton = '' mediaInstanceStr = '' blogsInstanceStr = '' newsInstanceStr = '' displayNickname = nickname bioStr = '' donateUrl = '' emailAddress = '' PGPpubKey = '' PGPfingerprint = '' xmppAddress = '' matrixAddress = '' ssbAddress = '' blogAddress = '' toxAddress = '' briarAddress = '' manuallyApprovesFollowers = '' movedTo = '' actorJson = loadJson(actorFilename) if actorJson: if actorJson.get('movedTo'): movedTo = actorJson['movedTo'] donateUrl = getDonationUrl(actorJson) xmppAddress = getXmppAddress(actorJson) matrixAddress = getMatrixAddress(actorJson) ssbAddress = getSSBAddress(actorJson) blogAddress = getBlogAddress(actorJson) toxAddress = getToxAddress(actorJson) briarAddress = getBriarAddress(actorJson) jamiAddress = getJamiAddress(actorJson) emailAddress = getEmailAddress(actorJson) PGPpubKey = getPGPpubKey(actorJson) PGPfingerprint = getPGPfingerprint(actorJson) if actorJson.get('name'): if not isFiltered(baseDir, nickname, domain, actorJson['name']): displayNickname = actorJson['name'] if actorJson.get('summary'): bioStr = \ actorJson['summary'].replace('

', '').replace('

', '') if isFiltered(baseDir, nickname, domain, bioStr): bioStr = '' if actorJson.get('manuallyApprovesFollowers'): if actorJson['manuallyApprovesFollowers']: manuallyApprovesFollowers = 'checked' else: manuallyApprovesFollowers = '' if actorJson.get('type'): if actorJson['type'] == 'Service': isBot = 'checked' isGroup = '' elif actorJson['type'] == 'Group': isGroup = 'checked' isBot = '' if os.path.isfile(baseDir + '/accounts/' + nickname + '@' + domain + '/.followDMs'): followDMs = 'checked' if os.path.isfile(baseDir + '/accounts/' + nickname + '@' + domain + '/.removeTwitter'): removeTwitter = 'checked' if os.path.isfile(baseDir + '/accounts/' + nickname + '@' + domain + '/.notifyLikes'): notifyLikes = 'checked' if os.path.isfile(baseDir + '/accounts/' + nickname + '@' + domain + '/.hideLikeButton'): hideLikeButton = 'checked' mediaInstance = getConfigParam(baseDir, "mediaInstance") if mediaInstance: if mediaInstance is True: mediaInstanceStr = 'checked' blogsInstanceStr = '' newsInstanceStr = '' newsInstance = getConfigParam(baseDir, "newsInstance") if newsInstance: if newsInstance is True: newsInstanceStr = 'checked' blogsInstanceStr = '' mediaInstanceStr = '' blogsInstance = getConfigParam(baseDir, "blogsInstance") if blogsInstance: if blogsInstance is True: blogsInstanceStr = 'checked' mediaInstanceStr = '' newsInstanceStr = '' filterStr = '' filterFilename = \ baseDir + '/accounts/' + nickname + '@' + domain + '/filters.txt' if os.path.isfile(filterFilename): with open(filterFilename, 'r') as filterfile: filterStr = filterfile.read() switchStr = '' switchFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/replacewords.txt' if os.path.isfile(switchFilename): with open(switchFilename, 'r') as switchfile: switchStr = switchfile.read() autoTags = '' autoTagsFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/autotags.txt' if os.path.isfile(autoTagsFilename): with open(autoTagsFilename, 'r') as autoTagsFile: autoTags = autoTagsFile.read() autoCW = '' autoCWFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/autocw.txt' if os.path.isfile(autoCWFilename): with open(autoCWFilename, 'r') as autoCWFile: autoCW = autoCWFile.read() blockedStr = '' blockedFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/blocking.txt' if os.path.isfile(blockedFilename): with open(blockedFilename, 'r') as blockedfile: blockedStr = blockedfile.read() dmAllowedInstancesStr = '' dmAllowedInstancesFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/dmAllowedInstances.txt' if os.path.isfile(dmAllowedInstancesFilename): with open(dmAllowedInstancesFilename, 'r') as dmAllowedInstancesFile: dmAllowedInstancesStr = dmAllowedInstancesFile.read() allowedInstancesStr = '' allowedInstancesFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/allowedinstances.txt' if os.path.isfile(allowedInstancesFilename): with open(allowedInstancesFilename, 'r') as allowedInstancesFile: allowedInstancesStr = allowedInstancesFile.read() gitProjectsStr = '' gitProjectsFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/gitprojects.txt' if os.path.isfile(gitProjectsFilename): with open(gitProjectsFilename, 'r') as gitProjectsFile: gitProjectsStr = gitProjectsFile.read() skills = getSkills(baseDir, nickname, domain) skillsStr = '' skillCtr = 1 if skills: for skillDesc, skillValue in skills.items(): if isFiltered(baseDir, nickname, domain, skillDesc): continue skillsStr += \ '

' skillsStr += \ '

' skillCtr += 1 skillsStr += \ '

' skillsStr += \ '

' skillsStr += ' \n' cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' themesDropdown = '' instanceStr = '' roleAssignStr = '' peertubeStr = '' adminNickname = getConfigParam(baseDir, 'admin') if isArtist(baseDir, nickname) or \ path.startswith('/users/' + str(adminNickname) + '/'): graphicsStr = '
' + \ translate['Graphic Design'] + '\n' graphicsStr += '
' # Themes section themes = getThemesList(baseDir) themesDropdown += '
\n' grayscaleFilename = \ baseDir + '/accounts/.grayscale' grayscale = '' if os.path.isfile(grayscaleFilename): grayscale = 'checked' themesDropdown += \ ' ' + translate['Grayscale'] + '
' themesDropdown += '
' if os.path.isfile(baseDir + '/fonts/custom.woff') or \ os.path.isfile(baseDir + '/fonts/custom.woff2') or \ os.path.isfile(baseDir + '/fonts/custom.otf') or \ os.path.isfile(baseDir + '/fonts/custom.ttf'): themesDropdown += \ ' ' + \ translate['Remove the custom font'] + '
' themeName = getConfigParam(baseDir, 'theme') themesDropdown = \ themesDropdown.replace('
\n' if adminNickname: if path.startswith('/users/' + adminNickname + '/'): # Instance details section instanceDescription = \ getConfigParam(baseDir, 'instanceDescription') instanceDescriptionShort = \ getConfigParam(baseDir, 'instanceDescriptionShort') instanceTitle = \ getConfigParam(baseDir, 'instanceTitle') instanceStr = '
' + \ translate['Instance Settings'] + '\n' instanceStr += '
' instanceStr += \ ' ' if instanceTitle: instanceStr += \ '
' else: instanceStr += \ '
' instanceStr += \ ' ' if instanceDescriptionShort: instanceStr += \ '
' else: instanceStr += \ '
' instanceStr += \ ' ' if instanceDescription: instanceStr += \ ' ' else: instanceStr += \ ' ' instanceStr += \ ' ' instanceStr += \ '
\n' instanceStr += \ '

\n' nodeInfoStr = \ translate['Show numbers of accounts within instance metadata'] if getConfigParam(baseDir, "showNodeInfoAccounts"): instanceStr += \ ' ' + \ nodeInfoStr + '
\n' else: instanceStr += \ ' ' + \ nodeInfoStr + '
\n' nodeInfoStr = \ translate['Show version number within instance metadata'] if getConfigParam(baseDir, "showNodeInfoVersion"): instanceStr += \ ' ' + \ nodeInfoStr + '
\n' else: instanceStr += \ ' ' + \ nodeInfoStr + '
\n' if getConfigParam(baseDir, "verifyAllSignatures"): instanceStr += \ ' ' + \ translate['Verify all signatures'] + '
\n' else: instanceStr += \ ' ' + \ translate['Verify all signatures'] + '
\n' instanceStr += translate['Enabling broch mode'] + '
\n' if getConfigParam(baseDir, "brochMode"): instanceStr += \ ' ' + \ translate['Broch mode'] + '
\n' else: instanceStr += \ ' ' + \ translate['Broch mode'] + '
\n' # Instance type instanceStr += \ '

\n' instanceStr += \ ' ' + \ translate['This is a media instance'] + '
\n' instanceStr += \ ' ' + \ translate['This is a blogging instance'] + '
\n' instanceStr += \ ' ' + \ translate['This is a news instance'] + '
\n' instanceStr += '
\n' # Role assignments section moderators = '' moderatorsFile = baseDir + '/accounts/moderators.txt' if os.path.isfile(moderatorsFile): with open(moderatorsFile, "r") as f: moderators = f.read() # site moderators roleAssignStr = '
' + \ translate['Role Assignment'] + '\n' roleAssignStr += '
' roleAssignStr += '
\n' roleAssignStr += ' ' + \ translate['A list of moderator nicknames. One per line.'] roleAssignStr += \ ' ' # site editors editors = '' editorsFile = baseDir + '/accounts/editors.txt' if os.path.isfile(editorsFile): with open(editorsFile, "r") as f: editors = f.read() roleAssignStr += '
\n' roleAssignStr += ' ' + \ translate['A list of editor nicknames. One per line.'] roleAssignStr += \ ' ' # counselors counselors = '' counselorsFile = baseDir + '/accounts/counselors.txt' if os.path.isfile(counselorsFile): with open(counselorsFile, "r") as f: counselors = f.read() roleAssignStr += '
\n' roleAssignStr += \ ' ' # artists artists = '' artistsFile = baseDir + '/accounts/artists.txt' if os.path.isfile(artistsFile): with open(artistsFile, "r") as f: artists = f.read() roleAssignStr += '
\n' roleAssignStr += \ ' ' roleAssignStr += '
\n' # Video section peertubeStr = '
' + \ translate['Video Settings'] + '\n' peertubeStr += '
\n' peertubeStr += \ ' \n' idx = 'Show video previews for the following Peertube sites.' peertubeStr += \ '
\n' peertubeInstancesStr = '' for url in peertubeInstances: peertubeInstancesStr += url + '\n' peertubeStr += \ ' \n' peertubeStr += \ '
\n' YTReplacementDomain = getConfigParam(baseDir, "youtubedomain") if not YTReplacementDomain: YTReplacementDomain = '' peertubeStr += \ ' \n' peertubeStr += '
\n' instanceTitle = \ getConfigParam(baseDir, 'instanceTitle') editProfileForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle) # keyboard navigation userPathStr = '/users/' + nickname userTimalineStr = '/users/' + nickname + '/' + defaultTimeline menuTimeline = \ htmlHideFromScreenReader('🏠') + ' ' + \ translate['Switch to timeline view'] menuProfile = \ htmlHideFromScreenReader('👤') + ' ' + \ translate['Switch to profile view'] navLinks = { menuProfile: userPathStr, menuTimeline: userTimalineStr } navAccessKeys = { menuProfile: 'p', menuTimeline: 't' } editProfileForm += htmlKeyboardNavigation(textModeBanner, navLinks, navAccessKeys) # top banner editProfileForm += \ '' editProfileForm += '\n' editProfileForm += \ '
\n' editProfileForm += '
\n' editProfileForm += \ '

' + translate['Profile for'] + \ ' ' + nickname + '@' + domainFull + '

' editProfileForm += '
\n' editProfileForm += \ '
\n' + \ ' \n' + \ '
\n' editProfileForm += '
\n' if scheduledPostsExist(baseDir, nickname, domain): editProfileForm += '
\n' editProfileForm += \ ' ' + \ translate['Remove scheduled posts'] + '
\n' editProfileForm += '
\n' editProfileForm += '
\n' editProfileForm += ' \n' editProfileForm += \ '
\n' editProfileForm += \ ' \n' editProfileForm += \ ' \n' editProfileForm += \ ' \n' editProfileForm += \ ' \n' occupationName = '' if actorJson.get('hasOccupation'): occupationName = getOccupationName(actorJson) editProfileForm += '
\n' editProfileForm += \ ' \n' alsoKnownAsStr = '' if actorJson.get('alsoKnownAs'): alsoKnownAs = actorJson['alsoKnownAs'] ctr = 0 for altActor in alsoKnownAs: if ctr > 0: alsoKnownAsStr += ', ' ctr += 1 alsoKnownAsStr += altActor editProfileForm += '
\n' editProfileForm += \ ' \n' editProfileForm += '
\n' editProfileForm += \ ' \n' editProfileForm += '
\n' editProfileForm += \ ' \n' editProfileForm += '
\n' editProfileForm += \ ' \n' editProfileForm += '
\n' # Option checkboxes editProfileForm += '
\n' editProfileForm += \ ' ' + translate['Approve follower requests'] + '
\n' editProfileForm += \ ' ' + translate['This is a bot account'] + '
\n' editProfileForm += \ ' ' + \ translate['This is a group account'] + '
\n' editProfileForm += \ ' ' + \ translate['Only people I follow can send me DMs'] + '
\n' editProfileForm += \ ' ' + \ translate['Remove Twitter posts'] + '
\n' editProfileForm += \ ' ' + \ translate['Notify when posts are liked'] + '
\n' editProfileForm += \ ' ' + \ translate["Don't show the Like button"] + '
\n' editProfileForm += '
\n' # Contact information editProfileForm += '
' + \ translate['Contact Details'] + '\n' editProfileForm += '
' editProfileForm += '
\n' editProfileForm += \ ' \n' editProfileForm += \ '
\n' editProfileForm += \ ' \n' editProfileForm += '
\n' editProfileForm += \ ' \n' editProfileForm += '
\n' editProfileForm += \ ' \n' editProfileForm += '
\n' editProfileForm += \ ' \n' editProfileForm += '
\n' editProfileForm += \ ' \n' editProfileForm += '
\n' editProfileForm += \ ' \n' editProfileForm += \ '
\n' editProfileForm += \ ' \n' editProfileForm += \ '
\n' editProfileForm += \ ' \n' editProfileForm += '
\n' editProfileForm += '
\n' # Customize images and banners editProfileForm += '
' + \ translate['Background Images'] + '\n' editProfileForm += '
\n' idx = 'The files attached below should be no larger than ' + \ '10MB in total uploaded at once.' editProfileForm += \ '

\n' if not newsInstance: editProfileForm += \ ' \n' editProfileForm += ' \n' editProfileForm += '
\n' editProfileForm += ' \n' editProfileForm += '
\n' editProfileForm += ' \n' editProfileForm += '
\n' editProfileForm += ' \n' editProfileForm += '
\n' editProfileForm += ' \n' editProfileForm += '
\n' # Change password editProfileForm += '
' + \ translate['Change Password'] + '\n' editProfileForm += '
\n' editProfileForm += \ '
\n' editProfileForm += '
\n' editProfileForm += \ '
\n' editProfileForm += \ ' \n' editProfileForm += '
\n' # Filtering and blocking section editProfileForm += '
' + \ translate['Filtering and Blocking'] + '\n' editProfileForm += '
\n' editProfileForm += \ '
\n' cityFilename = baseDir + '/accounts/' + \ nickname + '@' + domain + '/city.txt' if os.path.isfile(cityFilename): with open(cityFilename, 'r') as fp: city = fp.read().replace('\n', '') locationsFilename = baseDir + '/custom_locations.txt' if not os.path.isfile(locationsFilename): locationsFilename = baseDir + '/locations.txt' cities = [] with open(locationsFilename, "r") as f: cities = f.readlines() cities.sort() editProfileForm += '
\n' editProfileForm += \ ' \n' editProfileForm += '
\n' editProfileForm += ' \n' editProfileForm += \ '
\n' editProfileForm += '
\n' editProfileForm += \ ' \n' editProfileForm += \ '
\n' editProfileForm += '
\n' editProfileForm += \ ' \n' editProfileForm += \ '
\n' editProfileForm += '
\n' editProfileForm += \ ' \n' editProfileForm += \ '
\n' idx = 'Blocked accounts, one per line, in the form ' + \ 'nickname@domain or *@blockeddomain' editProfileForm += \ '
\n' editProfileForm += \ ' \n' editProfileForm += \ '
\n' idx = 'Direct messages are always allowed from these instances.' editProfileForm += \ '
\n' editProfileForm += \ ' \n' editProfileForm += \ '
\n' idx = 'Federate only with a defined set of instances. ' + \ 'One domain name per line.' editProfileForm += \ '
\n' editProfileForm += \ ' \n' editProfileForm += '
\n' editProfileForm += '
' + \ translate['Git Projects'] + '\n' editProfileForm += '
\n' idx = 'List of project names that you wish to receive git patches for' editProfileForm += \ ' \n' editProfileForm += \ ' \n' editProfileForm += '
\n' # Skills section editProfileForm += '
' + \ translate['Skills'] + '\n' editProfileForm += '
\n' editProfileForm += \ '
\n' 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 += ' \n' editProfileForm += skillsStr editProfileForm += roleAssignStr + peertubeStr + graphicsStr + instanceStr # danger zone section editProfileForm += '
' + \ translate['Danger Zone'] + '\n' editProfileForm += '
\n' editProfileForm += '
\n' editProfileForm += \ ' ' + \ translate['Deactivate this account'] + '
\n' editProfileForm += '
\n' editProfileForm += '
\n' editProfileForm += '\n' editProfileForm += htmlFooter() return editProfileForm def _individualFollowAsHtml(translate: {}, baseDir: str, session, cachedWebfingers: {}, personCache: {}, domain: str, followUrl: str, authorized: bool, actorNickname: str, httpPrefix: str, projectVersion: str, dormant: bool, debug: bool, buttons=[]) -> str: """An individual follow entry on the profile screen """ followUrlNickname = getNicknameFromActor(followUrl) followUrlDomain, followUrlPort = getDomainFromActor(followUrl) followUrlDomainFull = getFullDomain(followUrlDomain, followUrlPort) titleStr = '@' + followUrlNickname + '@' + followUrlDomainFull avatarUrl = getPersonAvatarUrl(baseDir, followUrl, personCache, True) if not avatarUrl: avatarUrl = followUrl + '/avatar.png' # lookup the correct webfinger for the followUrl followUrlHandle = followUrlNickname + '@' + followUrlDomainFull followUrlWf = \ webfingerHandle(session, followUrlHandle, httpPrefix, cachedWebfingers, domain, __version__, debug) (inboxUrl, pubKeyId, pubKey, fromPersonId, sharedInbox, avatarUrl2, displayName) = getPersonBox(baseDir, session, followUrlWf, personCache, projectVersion, httpPrefix, followUrlNickname, domain, 'outbox', 43036) if avatarUrl2: avatarUrl = avatarUrl2 if displayName: displayName = \ addEmojiToDisplayName(baseDir, httpPrefix, actorNickname, domain, displayName, False) titleStr = displayName if dormant: titleStr += ' 💤' buttonsStr = '' if authorized: for b in buttons: if b == 'block': buttonsStr += \ '\n' if b == 'unfollow': buttonsStr += \ '\n' resultStr = '
\n' resultStr += \ '\n' resultStr += '

 ' resultStr += titleStr + '' + buttonsStr + '

\n' resultStr += '
\n' return resultStr