__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 getLockedAccount
from utils import hasUsersPath
from utils import getFullDomain
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 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) -> 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:
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 = 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)
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']
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']
profileStr = \
_getProfileHeaderAfterSearch(baseDir,
nickname, defaultTimeline,
searchNickname,
searchDomainFull,
translate,
displayName, followsYou,
profileDescriptionShort,
avatarUrl, imageUrl,
movedTo, profileJson['id'],
alsoKnownAs)
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'
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,
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:
"""The header of the profile screen, containing background
image and avatar
"""
htmlStr = '\n\n \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: []) -> str:
"""The header of a searched for handle, containing background
image and avatar
"""
htmlStr = '\n\n \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,
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,
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 += \
' ' + translate['Donate'] + \
'
\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: ' + \
ssbAddress + '
\n'
if toxAddress:
donateSection += \
'Tox: ' + \
toxAddress + '
\n'
if briarAddress:
if briarAddress.startswith('briar://'):
donateSection += \
'' + \
briarAddress + '
\n'
else:
donateSection += \
'briar://' + \
briarAddress + '
\n'
if jamiAddress:
donateSection += \
'Jami: ' + \
jamiAddress + '
\n'
if PGPfingerprint:
donateSection += \
'PGP: ' + \
PGPfingerprint.replace('\n', ' ') + '
\n'
if PGPpubKey:
donateSection += \
'' + PGPpubKey.replace('\n', ' ') + '
\n'
donateSection += ' \n'
donateSection += '
\n'
if authorized:
editProfileStr = \
'' + \
' \n'
logoutStr = \
'' + \
' \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 += ''
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']
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)
# 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'
}
profileStr = htmlKeyboardNavigation(textModeBanner, navLinks)
profileStr += profileHeaderStr + donateSection
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 = \
'
' + \
' '
if selected == 'posts':
profileStr += \
_htmlProfilePosts(recentPostsCache, maxRecentPosts,
translate,
baseDir, httpPrefix, authorized,
nickname, domain, port,
session, cachedWebfingers, personCache,
projectVersion,
YTReplacementDomain,
showPublishedDateOnly,
peertubeInstances,
allowLocalNetworkAccess) + licenseStr
elif selected == 'following':
profileStr += \
_htmlProfileFollowing(translate, baseDir, httpPrefix,
authorized, nickname,
domain, port, session,
cachedWebfingers, personCache, extraJson,
projectVersion, ["unfollow"], selected,
usersPath, pageNumber, maxItemsPerPage,
dormantMonths)
elif selected == 'followers':
profileStr += \
_htmlProfileFollowing(translate, baseDir, httpPrefix,
authorized, nickname,
domain, port, session,
cachedWebfingers, personCache, extraJson,
projectVersion, ["block"],
selected, usersPath, pageNumber,
maxItemsPerPage, dormantMonths)
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 = \
htmlHeaderWithExternalStyle(cssFilename, instanceTitle) + \
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) -> 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,
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) -> str:
"""Shows following on the profile screen
"""
profileStr = ''
if authorized and pageNumber:
if authorized and pageNumber > 1:
# page up arrow
profileStr += \
' \n' + \
' \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,
buttons)
if authorized and maxItemsPerPage and pageNumber:
if len(followingJson['orderedItems']) >= maxItemsPerPage:
# page down arrow
profileStr += \
' \n' + \
' \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:
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 += \
'\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) -> str:
"""Shows the edit profile screen
"""
imageFormats = getImageFormats()
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()
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 += \
'
'
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
moderatorsStr = ''
themesDropdown = ''
instanceStr = ''
editorsStr = ''
peertubeStr = ''
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 += \
' ' + \
translate['Instance Title'] + ' '
if instanceTitle:
instanceStr += \
' '
else:
instanceStr += \
' '
instanceStr += \
' ' + \
translate['Instance Short Description'] + ' '
if instanceDescriptionShort:
instanceStr += \
' '
else:
instanceStr += \
' '
instanceStr += \
' ' + \
translate['Instance Description'] + ' '
if instanceDescription:
instanceStr += \
' '
else:
instanceStr += \
' '
instanceStr += \
' ' + \
translate['Instance Logo'] + ' '
instanceStr += \
' '
if getConfigParam(baseDir, "verifyAllSignatures"):
instanceStr += \
' ' + \
translate['Verify all signatures'] + ' \n'
else:
instanceStr += \
' ' + \
translate['Verify all signatures'] + ' \n'
if getConfigParam(baseDir, "brochMode"):
instanceStr += \
' ' + \
translate['Broch mode'] + ' \n'
else:
instanceStr += \
' ' + \
translate['Broch mode'] + ' \n'
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(baseDir)
themesDropdown = ''
themesDropdown += ' ' + translate['Theme'] + ' '
grayscaleFilename = \
baseDir + '/accounts/.grayscale'
grayscale = ''
if os.path.isfile(grayscaleFilename):
grayscale = 'checked'
themesDropdown += \
' ' + translate['Grayscale'] + ' '
themesDropdown += ' '
for themeName in themes:
themesDropdown += ' ' + \
translate[themeName] + ' '
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('',
' ')
peertubeStr = \
' ' + \
translate['Peertube Instances'] + ' \n'
idx = 'Show video previews for the following Peertube sites.'
peertubeStr += \
' ' + \
translate[idx] + ' \n'
peertubeInstancesStr = ''
for url in peertubeInstances:
peertubeInstancesStr += url + '\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
}
editProfileForm += htmlKeyboardNavigation(textModeBanner, navLinks)
# top banner
editProfileForm += \
''
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,
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__)
(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 += \
'' + \
translate['Block'] + ' \n'
if b == 'unfollow':
buttonsStr += \
'' + \
translate['Unfollow'] + ' \n'
resultStr = '\n'
return resultStr