__filename__ = "webapp_profile.py" __author__ = "Bob Mottram" __license__ = "AGPL3+" __version__ = "1.1.0" __maintainer__ = "Bob Mottram" __email__ = "bob@freedombone.net" __status__ = "Production" import os from pprint import pprint 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 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 webapp_utils import scheduledPostsExist from webapp_utils import getPersonAvatarUrl from webapp_utils import getIconsDir from webapp_utils import htmlHeaderWithExternalStyle from webapp_utils import htmlFooter from webapp_utils import addEmojiToDisplayName from webapp_utils import headerButtonsFrontScreen from webapp_utils import getBannerFile from webapp_utils import htmlPostSeparator from webapp_utils import getBlogAddress from webapp_post import individualPostAsHtml from webapp_column_left import getLeftColumnContent from webapp_column_right import getRightColumnContent 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) -> str: """Show a profile page after a search for a fediverse address """ if '/users/' in profileHandle or \ '/accounts/' in profileHandle or \ '/channel/' in profileHandle or \ '/profile/' in profileHandle or \ '/@' in profileHandle: searchNickname = getNicknameFromActor(profileHandle) searchDomain, searchPort = getDomainFromActor(profileHandle) else: if '@' not in profileHandle: print('DEBUG: no @ in ' + profileHandle) return None if profileHandle.startswith('@'): profileHandle = profileHandle[1:] if '@' not in profileHandle: 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: print('DEBUG: Search for handle ' + str(searchNickname) + '@' + str(searchDomain) + ':' + str(searchPort)) else: print('DEBUG: Search for handle ' + str(searchNickname) + '@' + str(searchDomain)) if not searchNickname: print('DEBUG: No nickname found in ' + profileHandle) return None if not searchDomain: print('DEBUG: No domain found in ' + profileHandle) return None searchDomainFull = searchDomain if searchPort: if searchPort != 80 and searchPort != 443: if ':' not in searchDomain: searchDomainFull = searchDomain + ':' + str(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) if not wf: 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): 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) if not personUrl: # try single user instance asHeader = { 'Accept': 'application/ld+json; profile="' + profileStr + '"' } personUrl = httpPrefix + '://' + searchDomainFull profileJson = \ getJson(session, personUrl, asHeader, None, projectVersion, httpPrefix, domain) if not profileJson: asHeader = { 'Accept': 'application/ld+json; profile="' + profileStr + '"' } profileJson = \ getJson(session, personUrl, asHeader, None, 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'] 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) profileStr = '
\n' profileStr += '
\n' if avatarUrl: profileStr += \ ' ' + avatarDescription + '\n' profileStr += '

' + displayName + '

\n' profileStr += '

@' + searchNickname + '@' + \ searchDomainFull + '

\n' profileStr += '

' + profileDescriptionShort + '

\n' profileStr += '
\n' profileStr += '
\n' profileStr += '
\n' profileStr += '
\n' profileStr += '
\n' profileStr += \ ' \n' profileStr += \ ' \n' profileStr += \ ' \n' profileStr += \ ' \n' profileStr += '
\n' profileStr += '
\n' profileStr += '
\n' iconsDir = getIconsDir(baseDir) i = 0 for item in parseUserFeed(session, outboxUrl, asHeader, projectVersion, httpPrefix, domain): if not item.get('type'): continue if item['type'] != 'Create' and item['type'] != 'Announce': continue if not item.get('object'): continue profileStr += \ individualPostAsHtml(True, recentPostsCache, maxRecentPosts, iconsDir, translate, None, baseDir, session, cachedWebfingers, personCache, nickname, domain, port, item, avatarUrl, False, False, httpPrefix, projectVersion, 'inbox', YTReplacementDomain, showPublishedDateOnly, False, False, False, False, False) i += 1 if i >= 20: break return htmlHeaderWithExternalStyle(cssFilename) + profileStr + htmlFooter() def getProfileHeader(baseDir: str, nickname: str, domain: str, domainFull: str, translate: {}, iconsDir: str, defaultTimeline: str, displayName: str, avatarDescription: str, profileDescriptionShort: str, loginButton: str, avatarUrl: str) -> str: """The header of the profile screen, containing background image and avatar """ htmlStr = '\n\n
\n' htmlStr += ' \n' htmlStr += ' \n' htmlStr += '
\n' htmlStr += \ ' ' + \
        avatarDescription + '\n' htmlStr += '

' + displayName + '

\n' htmlStr += \ '

@' + nickname + '@' + domainFull + '
\n' htmlStr += \ ' ' + \ '

\n' htmlStr += '

' + profileDescriptionShort + '

\n' htmlStr += loginButton 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, wfRequest: {}, personCache: {}, YTReplacementDomain: str, showPublishedDateOnly: bool, newswire: {}, extraJson=None, pageNumber=None, maxItemsPerPage=None) -> str: """Show the profile page as html """ nickname = profileJson['preferredUsername'] if not nickname: return "" 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) if donateUrl or xmppAddress or matrixAddress or \ ssbAddress or toxAddress 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 PGPfingerprint: donateSection += \ '

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

\n' if PGPpubKey: donateSection += \ '

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

\n' donateSection += '
\n' donateSection += '
\n' iconsDir = getIconsDir(baseDir) if not authorized: loginButton = headerButtonsFrontScreen(translate, nickname, 'features', authorized, iconsAsButtons, iconsDir) else: 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('

', '') # If this is the news account then show a different banner if isSystemAccount(nickname): bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) profileHeaderStr = \ '\n' if loginButton: profileHeaderStr += '
' + loginButton + '
\n' profileHeaderStr += '\n' profileHeaderStr += ' \n' profileHeaderStr += ' \n' profileHeaderStr += ' \n' profileHeaderStr += ' \n' profileHeaderStr += ' \n' profileHeaderStr += ' \n' profileHeaderStr += ' \n' profileHeaderStr += ' \n' profileHeaderStr += ' \n' profileFooterStr += ' \n' profileFooterStr += ' \n' profileFooterStr += ' \n' profileFooterStr += '
\n' iconsDir = getIconsDir(baseDir) profileHeaderStr += \ getLeftColumnContent(baseDir, 'news', domainFull, httpPrefix, translate, iconsDir, False, False, None, rssIconAtTop, True, True) profileHeaderStr += ' \n' else: avatarUrl = profileJson['icon']['url'] profileHeaderStr = \ getProfileHeader(baseDir, nickname, domain, domainFull, translate, iconsDir, defaultTimeline, displayName, avatarDescription, profileDescriptionShort, loginButton, avatarUrl) profileStr = profileHeaderStr + donateSection if not isSystemAccount(nickname): profileStr += '
\n' profileStr += '
' profileStr += \ ' ' profileStr += \ ' ' + \ '' profileStr += \ ' ' + \ '' profileStr += \ ' ' + \ '' profileStr += \ ' ' + \ '' profileStr += \ ' ' + \ '' profileStr += logoutStr + editProfileStr profileStr += '
' profileStr += '
' profileStr += followApprovalsSection cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' if isSystemAccount(nickname): bannerFile, bannerFilename = \ getBannerFile(baseDir, nickname, domain) # profileStyle = \ # profileStyle.replace('banner.png', # '/users/' + nickname + '/' + bannerFile) licenseStr = \ '' + \ '' + \
        translate['Get the source code'] + '' if selected == 'posts': profileStr += \ htmlProfilePosts(recentPostsCache, maxRecentPosts, translate, baseDir, httpPrefix, authorized, nickname, domain, port, session, wfRequest, personCache, projectVersion, YTReplacementDomain, showPublishedDateOnly) + licenseStr if selected == 'following': profileStr += \ htmlProfileFollowing(translate, baseDir, httpPrefix, authorized, nickname, domain, port, session, wfRequest, personCache, extraJson, projectVersion, ["unfollow"], selected, usersPath, pageNumber, maxItemsPerPage) if selected == 'followers': profileStr += \ htmlProfileFollowing(translate, baseDir, httpPrefix, authorized, nickname, domain, port, session, wfRequest, personCache, extraJson, projectVersion, ["block"], selected, usersPath, pageNumber, maxItemsPerPage) if selected == 'roles': profileStr += \ htmlProfileRoles(translate, nickname, domainFull, extraJson) if selected == 'skills': profileStr += \ htmlProfileSkills(translate, nickname, domainFull, extraJson) if selected == 'shares': profileStr += \ htmlProfileShares(actor, translate, nickname, domainFull, extraJson) + licenseStr # Footer which is only used for system accounts profileFooterStr = '' if isSystemAccount(nickname): profileFooterStr = '
\n' iconsDir = getIconsDir(baseDir) profileFooterStr += \ getRightColumnContent(baseDir, 'news', domainFull, httpPrefix, translate, iconsDir, False, False, newswire, False, False, None, False, False, False, True, authorized, True) profileFooterStr += '
\n' profileStr = \ htmlHeaderWithExternalStyle(cssFilename) + \ profileStr + profileFooterStr + htmlFooter() return profileStr def htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int, translate: {}, baseDir: str, httpPrefix: str, authorized: bool, nickname: str, domain: str, port: int, session, wfRequest: {}, personCache: {}, projectVersion: str, YTReplacementDomain: str, showPublishedDateOnly: bool) -> str: """Shows posts on the profile screen These should only be public posts """ iconsDir = getIconsDir(baseDir) separatorStr = htmlPostSeparator(baseDir, None) profileStr = '' maxItems = 4 ctr = 0 currPage = 1 while ctr < maxItems and currPage < 4: outboxFeed = \ personBoxJson({}, session, baseDir, domain, port, '/users/' + nickname + '/outbox?page=' + str(currPage), httpPrefix, 10, 'outbox', 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, iconsDir, translate, None, baseDir, session, wfRequest, personCache, nickname, domain, port, item, None, True, False, httpPrefix, projectVersion, 'inbox', YTReplacementDomain, showPublishedDateOnly, False, False, False, True, False) if postStr: profileStr += separatorStr + postStr 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, wfRequest: {}, personCache: {}, followingJson: {}, projectVersion: str, buttons: [], feedName: str, actor: str, pageNumber: int, maxItemsPerPage: int) -> str: """Shows following on the profile screen """ profileStr = '' iconsDir = getIconsDir(baseDir) if authorized and pageNumber: if authorized and pageNumber > 1: # page up arrow profileStr += \ '
\n' + \ ' ' + \
                translate['Page up'] + '\n' + \ '
\n' for item in followingJson['orderedItems']: profileStr += \ individualFollowAsHtml(translate, baseDir, session, wfRequest, personCache, domain, item, authorized, nickname, httpPrefix, projectVersion, 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, rolesJson: {}) -> str: """Shows roles on the profile screen """ profileStr = '' for project, rolesList in rolesJson.items(): profileStr += \ '
\n

' + project + \ '

\n
\n' for role in rolesList: 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) -> str: """Shows the edit profile screen """ imageFormats = '.png, .jpg, .jpeg, .gif, .webp, .avif' path = path.replace('/inbox', '').replace('/outbox', '') path = path.replace('/shares', '') nickname = getNicknameFromActor(path) if not nickname: return '' domainFull = domain if port: if port != 80 and port != 443: if ':' not in domain: domainFull = domain + ':' + str(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) isBot = '' isGroup = '' followDMs = '' removeTwitter = '' notifyLikes = '' hideLikeButton = '' mediaInstanceStr = '' blogsInstanceStr = '' newsInstanceStr = '' displayNickname = nickname bioStr = '' donateUrl = '' emailAddress = '' PGPpubKey = '' PGPfingerprint = '' xmppAddress = '' matrixAddress = '' ssbAddress = '' blogAddress = '' toxAddress = '' manuallyApprovesFollowers = '' actorJson = loadJson(actorFilename) if actorJson: donateUrl = getDonationUrl(actorJson) xmppAddress = getXmppAddress(actorJson) matrixAddress = getMatrixAddress(actorJson) ssbAddress = getSSBAddress(actorJson) blogAddress = getBlogAddress(actorJson) toxAddress = getToxAddress(actorJson) emailAddress = getEmailAddress(actorJson) PGPpubKey = getPGPpubKey(actorJson) PGPfingerprint = getPGPfingerprint(actorJson) if actorJson.get('name'): displayNickname = actorJson['name'] if actorJson.get('summary'): bioStr = \ actorJson['summary'].replace('

', '').replace('

', '') 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() 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(): skillsStr += \ '

' skillsStr += \ '

' skillCtr += 1 skillsStr += \ '

' skillsStr += \ '

' cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' moderatorsStr = '' themesDropdown = '' instanceStr = '' adminNickname = getConfigParam(baseDir, 'admin') if adminNickname: if path.startswith('/users/' + adminNickname + '/'): instanceDescription = \ getConfigParam(baseDir, 'instanceDescription') instanceDescriptionShort = \ getConfigParam(baseDir, 'instanceDescriptionShort') instanceTitle = \ getConfigParam(baseDir, 'instanceTitle') instanceStr += '
' instanceStr += \ ' ' if instanceTitle: instanceStr += \ '
' else: instanceStr += \ '
' instanceStr += \ ' ' if instanceDescriptionShort: instanceStr += \ '
' else: instanceStr += \ '
' instanceStr += \ ' ' if instanceDescription: instanceStr += \ ' ' else: instanceStr += \ ' ' instanceStr += \ ' ' instanceStr += \ ' ' instanceStr += '
' moderators = '' moderatorsFile = baseDir + '/accounts/moderators.txt' if os.path.isfile(moderatorsFile): with open(moderatorsFile, "r") as f: moderators = f.read() moderatorsStr = '
' moderatorsStr += ' ' + translate['Moderators'] + '
' moderatorsStr += ' ' + \ translate['A list of moderator nicknames. One per line.'] moderatorsStr += \ ' ' moderatorsStr += '
' editors = '' editorsFile = baseDir + '/accounts/editors.txt' if os.path.isfile(editorsFile): with open(editorsFile, "r") as f: editors = f.read() editorsStr = '
' editorsStr += ' ' + translate['Site Editors'] + '
' editorsStr += ' ' + \ translate['A list of editor nicknames. One per line.'] editorsStr += \ ' ' editorsStr += '
' themes = getThemesList() themesDropdown = '
' themesDropdown += ' ' + translate['Theme'] + '
' 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'] + '
' themesDropdown += '
' themeName = getConfigParam(baseDir, 'theme') themesDropdown = \ themesDropdown.replace('