mirror of https://gitlab.com/bashrc2/epicyon
2158 lines
86 KiB
Python
2158 lines
86 KiB
Python
__filename__ = "webapp_profile.py"
|
|
__author__ = "Bob Mottram"
|
|
__license__ = "AGPL3+"
|
|
__version__ = "1.2.0"
|
|
__maintainer__ = "Bob Mottram"
|
|
__email__ = "bob@freedombone.net"
|
|
__status__ = "Production"
|
|
__module_group__ = "Web Interface"
|
|
|
|
import os
|
|
from pprint import pprint
|
|
from utils import hasObjectDict
|
|
from utils import getOccupationName
|
|
from utils import getLockedAccount
|
|
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 utils import acctDir
|
|
from utils import getSupportedLanguages
|
|
from utils import localActorUrl
|
|
from languages import getActorLanguages
|
|
from skills import getSkills
|
|
from theme import getThemesList
|
|
from person import personBoxJson
|
|
from person import getActorJson
|
|
from person import getPersonAvatarUrl
|
|
from webfinger import webfingerHandle
|
|
from posts import parseUserFeed
|
|
from posts import getPersonBox
|
|
from donate import getDonationUrl
|
|
from donate import getWebsite
|
|
from xmpp import getXmppAddress
|
|
from matrix import getMatrixAddress
|
|
from ssb import getSSBAddress
|
|
from pgp import getEmailAddress
|
|
from pgp import getPGPfingerprint
|
|
from pgp import getPGPpubKey
|
|
from tox import getToxAddress
|
|
from briar import getBriarAddress
|
|
from jami import getJamiAddress
|
|
from cwtch import getCwtchAddress
|
|
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 htmlHeaderWithExternalStyle
|
|
from webapp_utils import htmlHeaderWithPersonMarkup
|
|
from webapp_utils import htmlFooter
|
|
from webapp_utils import addEmojiToDisplayName
|
|
from webapp_utils import getBannerFile
|
|
from webapp_utils import htmlPostSeparator
|
|
from webapp_utils import editCheckBox
|
|
from webapp_utils import editTextField
|
|
from webapp_utils import editTextArea
|
|
from webapp_utils import beginEditSection
|
|
from webapp_utils import endEditSection
|
|
from blog import getBlogAddress
|
|
from webapp_post import individualPostAsHtml
|
|
from webapp_timeline import htmlIndividualShare
|
|
|
|
|
|
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: {},
|
|
systemLanguage: str,
|
|
maxLikeCount: int,
|
|
signingPrivateKeyPem: str) -> str:
|
|
"""Show a profile page after a search for a fediverse address
|
|
"""
|
|
http = False
|
|
gnunet = False
|
|
if httpPrefix == 'http':
|
|
http = True
|
|
elif httpPrefix == 'gnunet':
|
|
gnunet = True
|
|
profileJson, asHeader = \
|
|
getActorJson(domain, profileHandle, http, gnunet, debug, False,
|
|
signingPrivateKeyPem)
|
|
if not profileJson:
|
|
return None
|
|
|
|
personUrl = profileJson['id']
|
|
searchDomain, searchPort = getDomainFromActor(personUrl)
|
|
searchNickname = getNicknameFromActor(personUrl)
|
|
searchDomainFull = getFullDomain(searchDomain, searchPort)
|
|
|
|
profileStr = ''
|
|
cssFilename = baseDir + '/epicyon-profile.css'
|
|
if os.path.isfile(baseDir + '/epicyon.css'):
|
|
cssFilename = baseDir + '/epicyon.css'
|
|
|
|
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']
|
|
if '"' in movedTo:
|
|
movedTo = movedTo.split('"')[1]
|
|
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 '<br>' in profileDescription:
|
|
if len(profileDescription.split('<br>')) > 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('<br>', '\n')
|
|
avatarDescription = avatarDescription.replace('<p>', '')
|
|
avatarDescription = avatarDescription.replace('</p>', '')
|
|
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 not profileJson.get('followers'):
|
|
# no followers collection specified within actor
|
|
followIsPermitted = False
|
|
elif 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 += \
|
|
'<div class="container">\n' + \
|
|
' <form method="POST" action="' + \
|
|
backUrl + '/followconfirm">\n' + \
|
|
' <center>\n' + \
|
|
' <input type="hidden" name="actor" value="' + \
|
|
personUrl + '">\n' + \
|
|
' <button type="submit" class="button" name="submitYes" ' + \
|
|
'accesskey="' + accessKeys['followButton'] + '">' + \
|
|
translate['Follow'] + '</button>\n' + \
|
|
' <button type="submit" class="button" name="submitView" ' + \
|
|
'accesskey="' + accessKeys['viewButton'] + '">' + \
|
|
translate['View'] + '</button>\n' + \
|
|
' </center>\n' + \
|
|
' </form>\n' + \
|
|
'</div>\n'
|
|
else:
|
|
profileStr += \
|
|
'<div class="container">\n' + \
|
|
' <form method="POST" action="' + \
|
|
backUrl + '/followconfirm">\n' + \
|
|
' <center>\n' + \
|
|
' <input type="hidden" name="actor" value="' + \
|
|
personUrl + '">\n' + \
|
|
' <button type="submit" class="button" name="submitView" ' + \
|
|
'accesskey="' + accessKeys['viewButton'] + '">' + \
|
|
translate['View'] + '</button>\n' + \
|
|
' </center>\n' + \
|
|
' </form>\n' + \
|
|
'</div>\n'
|
|
|
|
userFeed = \
|
|
parseUserFeed(signingPrivateKeyPem,
|
|
session, outboxUrl, asHeader, projectVersion,
|
|
httpPrefix, domain, debug)
|
|
if userFeed:
|
|
i = 0
|
|
for item in userFeed:
|
|
if not item.get('actor'):
|
|
continue
|
|
if item['actor'] != personUrl:
|
|
continue
|
|
if not item.get('type'):
|
|
continue
|
|
if item['type'] != 'Create':
|
|
continue
|
|
if not hasObjectDict(item):
|
|
continue
|
|
|
|
profileStr += \
|
|
individualPostAsHtml(signingPrivateKeyPem,
|
|
True, recentPostsCache, maxRecentPosts,
|
|
translate, None, baseDir,
|
|
session, cachedWebfingers, personCache,
|
|
nickname, domain, port,
|
|
item, avatarUrl, False, False,
|
|
httpPrefix, projectVersion, 'inbox',
|
|
YTReplacementDomain,
|
|
showPublishedDateOnly,
|
|
peertubeInstances,
|
|
allowLocalNetworkAccess,
|
|
themeName, systemLanguage, maxLikeCount,
|
|
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 <figure class="profileHeader">\n' + \
|
|
' <a href="/users/' + \
|
|
nickname + '/' + defaultTimeline + '" title="' + \
|
|
translate['Switch to timeline view'] + '">\n' + \
|
|
' <img class="profileBackground" ' + \
|
|
'alt="" ' + \
|
|
'src="/users/' + nickname + '/image_' + theme + '.png" /></a>\n' + \
|
|
' <figcaption>\n' + \
|
|
' <a href="/users/' + \
|
|
nickname + '/' + defaultTimeline + '" title="' + \
|
|
translate['Switch to timeline view'] + '">\n' + \
|
|
' <img loading="lazy" src="' + avatarUrl + '" ' + \
|
|
'alt="" class="title"></a>\n'
|
|
|
|
occupationStr = ''
|
|
if occupationName:
|
|
occupationStr += \
|
|
' <b>' + occupationName + '</b><br>\n'
|
|
|
|
htmlStr += ' <h1>' + displayName + '</h1>\n' + occupationStr
|
|
|
|
htmlStr += \
|
|
' <p><b>@' + nickname + '@' + domainFull + '</b><br>\n'
|
|
if joinedDate:
|
|
htmlStr += \
|
|
' <p>' + translate['Joined'] + ' ' + \
|
|
joinedDate.split('T')[0] + '<br>\n'
|
|
if movedTo:
|
|
newNickname = getNicknameFromActor(movedTo)
|
|
newDomain, newPort = getDomainFromActor(movedTo)
|
|
newDomainFull = getFullDomain(newDomain, newPort)
|
|
if newNickname and newDomain:
|
|
htmlStr += \
|
|
' <p>' + translate['New account'] + ': ' + \
|
|
'<a href="' + movedTo + '">@' + \
|
|
newNickname + '@' + newDomainFull + '</a><br>\n'
|
|
elif alsoKnownAs:
|
|
otherAccountsHtml = \
|
|
' <p>' + translate['Other accounts'] + ': '
|
|
|
|
actor = localActorUrl(httpPrefix, nickname, domainFull)
|
|
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 += \
|
|
'<a href="' + altActor + '">' + altDomain + '</a>'
|
|
elif isinstance(alsoKnownAs, str):
|
|
if alsoKnownAs != actor:
|
|
ctr += 1
|
|
altDomain, altPort = getDomainFromActor(alsoKnownAs)
|
|
otherAccountsHtml += \
|
|
'<a href="' + alsoKnownAs + '">' + altDomain + '</a>'
|
|
otherAccountsHtml += '</p>\n'
|
|
if ctr > 0:
|
|
htmlStr += otherAccountsHtml
|
|
htmlStr += \
|
|
' <a href="/users/' + nickname + \
|
|
'/qrcode.png" alt="' + translate['QR Code'] + '" title="' + \
|
|
translate['QR Code'] + '">' + \
|
|
'<img class="qrcode" alt="' + translate['QR Code'] + \
|
|
'" src="/icons/qrcode.png" /></a></p>\n' + \
|
|
' <p>' + profileDescriptionShort + '</p>\n' + loginButton
|
|
if pinnedContent:
|
|
htmlStr += pinnedContent.replace('<p>', '<p>📎', 1)
|
|
htmlStr += \
|
|
' </figcaption>\n' + \
|
|
' </figure>\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 <figure class="profileHeader">\n' + \
|
|
' <a href="/users/' + \
|
|
nickname + '/' + defaultTimeline + '" title="' + \
|
|
translate['Switch to timeline view'] + '" ' + \
|
|
'accesskey="' + accessKeys['menuTimeline'] + '">\n' + \
|
|
' <img class="profileBackground" ' + \
|
|
'alt="" ' + \
|
|
'src="' + imageUrl + '" /></a>\n' + \
|
|
' <figcaption>\n'
|
|
if avatarUrl:
|
|
htmlStr += \
|
|
' <a href="/users/' + \
|
|
nickname + '/' + defaultTimeline + '" title="' + \
|
|
translate['Switch to timeline view'] + '">\n' + \
|
|
' <img loading="lazy" src="' + avatarUrl + '" ' + \
|
|
'alt="" class="title"></a>\n'
|
|
htmlStr += \
|
|
' <h1>' + displayName + '</h1>\n' + \
|
|
' <p><b>@' + searchNickname + '@' + searchDomainFull + '</b><br>\n'
|
|
if joinedDate:
|
|
htmlStr += ' <p>' + translate['Joined'] + ' ' + \
|
|
joinedDate.split('T')[0] + '</p>\n'
|
|
if followsYou:
|
|
htmlStr += ' <p><b>' + translate['Follows you'] + '</b></p>\n'
|
|
if movedTo:
|
|
newNickname = getNicknameFromActor(movedTo)
|
|
newDomain, newPort = getDomainFromActor(movedTo)
|
|
newDomainFull = getFullDomain(newDomain, newPort)
|
|
if newNickname and newDomain:
|
|
newHandle = newNickname + '@' + newDomainFull
|
|
htmlStr += ' <p>' + translate['New account'] + \
|
|
': <a href="' + movedTo + '">@' + newHandle + '</a></p>\n'
|
|
elif alsoKnownAs:
|
|
otherAccountshtml = \
|
|
' <p>' + 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 += \
|
|
'<a href="' + altActor + '">' + altDomain + '</a>'
|
|
elif isinstance(alsoKnownAs, str):
|
|
if alsoKnownAs != actor:
|
|
ctr += 1
|
|
altDomain, altPort = getDomainFromActor(alsoKnownAs)
|
|
otherAccountshtml += \
|
|
'<a href="' + alsoKnownAs + '">' + altDomain + '</a>'
|
|
|
|
otherAccountshtml += '</p>\n'
|
|
if ctr > 0:
|
|
htmlStr += otherAccountshtml
|
|
|
|
htmlStr += \
|
|
' <p>' + profileDescriptionShort + '</p>\n' + \
|
|
' </figcaption>\n' + \
|
|
' </figure>\n\n'
|
|
return htmlStr
|
|
|
|
|
|
def htmlProfile(signingPrivateKeyPem: str,
|
|
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,
|
|
systemLanguage: str, maxLikeCount: int,
|
|
sharedItemsFederatedDomains: [],
|
|
extraJson: {} = None, pageNumber: int = None,
|
|
maxItemsPerPage: int = None) -> str:
|
|
"""Show the profile page as html
|
|
"""
|
|
nickname = profileJson['preferredUsername']
|
|
if not nickname:
|
|
return ""
|
|
if isSystemAccount(nickname):
|
|
return htmlFrontScreen(signingPrivateKeyPem,
|
|
rssIconAtTop,
|
|
cssCache, iconsAsButtons,
|
|
defaultTimeline,
|
|
recentPostsCache, maxRecentPosts,
|
|
translate, projectVersion,
|
|
baseDir, httpPrefix, authorized,
|
|
profileJson, selected,
|
|
session, cachedWebfingers, personCache,
|
|
YTReplacementDomain,
|
|
showPublishedDateOnly,
|
|
newswire, theme, extraJson,
|
|
allowLocalNetworkAccess, accessKeys,
|
|
systemLanguage, maxLikeCount,
|
|
sharedItemsFederatedDomains,
|
|
pageNumber, maxItemsPerPage)
|
|
|
|
domain, port = getDomainFromActor(profileJson['id'])
|
|
if not domain:
|
|
return ""
|
|
displayName = \
|
|
addEmojiToDisplayName(baseDir, httpPrefix,
|
|
nickname, domain,
|
|
profileJson['name'], True)
|
|
domainFull = getFullDomain(domain, port)
|
|
profileDescription = \
|
|
addEmojiToDisplayName(baseDir, httpPrefix,
|
|
nickname, domain,
|
|
profileJson['summary'], False)
|
|
postsButton = 'button'
|
|
followingButton = 'button'
|
|
followersButton = 'button'
|
|
rolesButton = 'button'
|
|
skillsButton = 'button'
|
|
sharesButton = 'button'
|
|
wantedButton = 'button'
|
|
if selected == 'posts':
|
|
postsButton = 'buttonselected'
|
|
elif selected == 'following':
|
|
followingButton = 'buttonselected'
|
|
elif selected == 'followers':
|
|
followersButton = 'buttonselected'
|
|
elif selected == 'roles':
|
|
rolesButton = 'buttonselected'
|
|
elif selected == 'skills':
|
|
skillsButton = 'buttonselected'
|
|
elif selected == 'shares':
|
|
sharesButton = 'buttonselected'
|
|
elif selected == 'wanted':
|
|
wantedButton = 'buttonselected'
|
|
loginButton = ''
|
|
|
|
followApprovalsSection = ''
|
|
followApprovals = False
|
|
editProfileStr = ''
|
|
logoutStr = ''
|
|
actor = profileJson['id']
|
|
usersPath = '/users/' + actor.split('/users/')[1]
|
|
|
|
donateSection = ''
|
|
donateUrl = getDonationUrl(profileJson)
|
|
websiteUrl = getWebsite(profileJson, translate)
|
|
blogAddress = getBlogAddress(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)
|
|
cwtchAddress = getCwtchAddress(profileJson)
|
|
if donateUrl or websiteUrl or xmppAddress or matrixAddress or \
|
|
ssbAddress or toxAddress or briarAddress or \
|
|
jamiAddress or cwtchAddress or PGPpubKey or \
|
|
PGPfingerprint or emailAddress:
|
|
donateSection = '<div class="container">\n'
|
|
donateSection += ' <center>\n'
|
|
if donateUrl and not isSystemAccount(nickname):
|
|
donateSection += \
|
|
' <p><a href="' + donateUrl + \
|
|
'"><button class="donateButton">' + translate['Donate'] + \
|
|
'</button></a></p>\n'
|
|
if websiteUrl:
|
|
donateSection += \
|
|
'<p>' + translate['Website'] + ': <a href="' + \
|
|
websiteUrl + '">' + websiteUrl + '</a></p>\n'
|
|
if emailAddress:
|
|
donateSection += \
|
|
'<p>' + translate['Email'] + ': <a href="mailto:' + \
|
|
emailAddress + '">' + emailAddress + '</a></p>\n'
|
|
if blogAddress:
|
|
donateSection += \
|
|
'<p>Blog: <a href="' + \
|
|
blogAddress + '">' + blogAddress + '</a></p>\n'
|
|
if xmppAddress:
|
|
donateSection += \
|
|
'<p>' + translate['XMPP'] + ': <a href="xmpp:' + \
|
|
xmppAddress + '">' + xmppAddress + '</a></p>\n'
|
|
if matrixAddress:
|
|
donateSection += \
|
|
'<p>' + translate['Matrix'] + ': ' + matrixAddress + '</p>\n'
|
|
if ssbAddress:
|
|
donateSection += \
|
|
'<p>SSB: <label class="ssbaddr">' + \
|
|
ssbAddress + '</label></p>\n'
|
|
if toxAddress:
|
|
donateSection += \
|
|
'<p>Tox: <label class="toxaddr">' + \
|
|
toxAddress + '</label></p>\n'
|
|
if briarAddress:
|
|
if briarAddress.startswith('briar://'):
|
|
donateSection += \
|
|
'<p><label class="toxaddr">' + \
|
|
briarAddress + '</label></p>\n'
|
|
else:
|
|
donateSection += \
|
|
'<p>briar://<label class="toxaddr">' + \
|
|
briarAddress + '</label></p>\n'
|
|
if jamiAddress:
|
|
donateSection += \
|
|
'<p>Jami: <label class="toxaddr">' + \
|
|
jamiAddress + '</label></p>\n'
|
|
if cwtchAddress:
|
|
donateSection += \
|
|
'<p>Cwtch: <label class="toxaddr">' + \
|
|
cwtchAddress + '</label></p>\n'
|
|
if PGPfingerprint:
|
|
donateSection += \
|
|
'<p class="pgp">PGP: ' + \
|
|
PGPfingerprint.replace('\n', '<br>') + '</p>\n'
|
|
if PGPpubKey:
|
|
donateSection += \
|
|
'<p class="pgp">' + PGPpubKey.replace('\n', '<br>') + '</p>\n'
|
|
donateSection += ' </center>\n'
|
|
donateSection += '</div>\n'
|
|
|
|
if authorized:
|
|
editProfileStr = \
|
|
'<a class="imageAnchor" href="' + usersPath + '/editprofile">' + \
|
|
'<img loading="lazy" src="/icons' + \
|
|
'/edit.png" title="' + translate['Edit'] + \
|
|
'" alt="| ' + translate['Edit'] + '" class="timelineicon"/></a>\n'
|
|
|
|
logoutStr = \
|
|
'<a class="imageAnchor" href="/logout">' + \
|
|
'<img loading="lazy" src="/icons' + \
|
|
'/logout.png" title="' + translate['Logout'] + \
|
|
'" alt="| ' + translate['Logout'] + \
|
|
'" class="timelineicon"/></a>\n'
|
|
|
|
# are there any follow requests?
|
|
followRequestsFilename = \
|
|
acctDir(baseDir, 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:
|
|
nick = followerHandle.split('@')[0]
|
|
dom = followerHandle.split('@')[1]
|
|
followerActor = \
|
|
localActorUrl(httpPrefix, nick, dom)
|
|
basePath = '/users/' + nickname
|
|
followApprovalsSection += '<div class="container">'
|
|
followApprovalsSection += \
|
|
'<a href="' + followerActor + '">'
|
|
followApprovalsSection += \
|
|
'<span class="followRequestHandle">' + \
|
|
followerHandle + '</span></a>'
|
|
followApprovalsSection += \
|
|
'<a href="' + basePath + \
|
|
'/followapprove=' + followerHandle + '">'
|
|
followApprovalsSection += \
|
|
'<button class="followApprove">' + \
|
|
translate['Approve'] + '</button></a><br><br>'
|
|
followApprovalsSection += \
|
|
'<a href="' + basePath + \
|
|
'/followdeny=' + followerHandle + '">'
|
|
followApprovalsSection += \
|
|
'<button class="followDeny">' + \
|
|
translate['Deny'] + '</button></a>'
|
|
followApprovalsSection += '</div>'
|
|
|
|
profileDescriptionShort = profileDescription
|
|
if '\n' in profileDescription:
|
|
if len(profileDescription.split('\n')) > 2:
|
|
profileDescriptionShort = ''
|
|
else:
|
|
if '<br>' in profileDescription:
|
|
if len(profileDescription.split('<br>')) > 2:
|
|
profileDescriptionShort = ''
|
|
profileDescription = profileDescription.replace('<br>', '\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('<br>', '\n')
|
|
avatarDescription = avatarDescription.replace('<p>', '')
|
|
avatarDescription = avatarDescription.replace('</p>', '')
|
|
|
|
movedTo = ''
|
|
if profileJson.get('movedTo'):
|
|
movedTo = profileJson['movedTo']
|
|
if '"' in movedTo:
|
|
movedTo = movedTo.split('"')[1]
|
|
|
|
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']
|
|
# use alternate path for local avatars to avoid any caching issues
|
|
if '://' + domainFull + '/accounts/avatars/' in avatarUrl:
|
|
avatarUrl = \
|
|
avatarUrl.replace('://' + domainFull + '/accounts/avatars/',
|
|
'://' + domainFull + '/users/')
|
|
|
|
# get pinned post content
|
|
accountDir = acctDir(baseDir, 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']
|
|
menuLogout = \
|
|
htmlHideFromScreenReader('❎') + ' ' + translate['Logout']
|
|
navLinks = {
|
|
menuTimeline: userPathStr + '/' + deft,
|
|
menuEdit: userPathStr + '/editprofile',
|
|
menuFollowing: userPathStr + '/following#timeline',
|
|
menuFollowers: userPathStr + '/followers#timeline',
|
|
menuRoles: userPathStr + '/roles#timeline',
|
|
menuSkills: userPathStr + '/skills#timeline',
|
|
menuLogout: '/logout'
|
|
}
|
|
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 += '<div class="container" id="buttonheader">\n'
|
|
profileStr += ' <center>'
|
|
profileStr += \
|
|
' <a href="' + usersPath + '#buttonheader"><button class="' + \
|
|
postsButton + '"><span>' + translate['Posts'] + \
|
|
' </span></button></a>'
|
|
profileStr += \
|
|
' <a href="' + usersPath + '/following#buttonheader">' + \
|
|
'<button class="' + followingButton + '"><span>' + \
|
|
translate['Following'] + ' </span></button></a>'
|
|
profileStr += \
|
|
' <a href="' + usersPath + '/followers#buttonheader">' + \
|
|
'<button class="' + followersButton + \
|
|
'"><span>' + translate['Followers'] + ' </span></button></a>'
|
|
profileStr += \
|
|
' <a href="' + usersPath + '/roles#buttonheader">' + \
|
|
'<button class="' + rolesButton + '"><span>' + \
|
|
translate['Roles'] + \
|
|
' </span></button></a>'
|
|
profileStr += \
|
|
' <a href="' + usersPath + '/skills#buttonheader">' + \
|
|
'<button class="' + skillsButton + '"><span>' + \
|
|
translate['Skills'] + ' </span></button></a>'
|
|
# profileStr += \
|
|
# ' <a href="' + usersPath + '/shares#buttonheader">' + \
|
|
# '<button class="' + sharesButton + '"><span>' + \
|
|
# translate['Shares'] + ' </span></button></a>'
|
|
# profileStr += \
|
|
# ' <a href="' + usersPath + '/wanted#buttonheader">' + \
|
|
# '<button class="' + wantedButton + '"><span>' + \
|
|
# translate['Wanted'] + ' </span></button></a>'
|
|
profileStr += logoutStr + editProfileStr
|
|
profileStr += ' </center>'
|
|
profileStr += '</div>'
|
|
|
|
# start of #timeline
|
|
profileStr += '<div id="timeline">\n'
|
|
|
|
profileStr += followApprovalsSection
|
|
|
|
cssFilename = baseDir + '/epicyon-profile.css'
|
|
if os.path.isfile(baseDir + '/epicyon.css'):
|
|
cssFilename = baseDir + '/epicyon.css'
|
|
|
|
licenseStr = \
|
|
'<a href="https://gitlab.com/bashrc2/epicyon">' + \
|
|
'<img loading="lazy" class="license" alt="' + \
|
|
translate['Get the source code'] + '" title="' + \
|
|
translate['Get the source code'] + '" src="/icons/agpl.png" /></a>'
|
|
|
|
if selected == 'posts':
|
|
profileStr += \
|
|
_htmlProfilePosts(recentPostsCache, maxRecentPosts,
|
|
translate,
|
|
baseDir, httpPrefix, authorized,
|
|
nickname, domain, port,
|
|
session, cachedWebfingers, personCache,
|
|
projectVersion,
|
|
YTReplacementDomain,
|
|
showPublishedDateOnly,
|
|
peertubeInstances,
|
|
allowLocalNetworkAccess,
|
|
theme, systemLanguage,
|
|
maxLikeCount,
|
|
signingPrivateKeyPem) + 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, signingPrivateKeyPem)
|
|
elif selected == 'followers':
|
|
profileStr += \
|
|
_htmlProfileFollowing(translate, baseDir, httpPrefix,
|
|
authorized, nickname,
|
|
domain, port, session,
|
|
cachedWebfingers, personCache, extraJson,
|
|
projectVersion, ["block"],
|
|
selected, usersPath, pageNumber,
|
|
maxItemsPerPage, dormantMonths, debug,
|
|
signingPrivateKeyPem)
|
|
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, 'shares') + licenseStr
|
|
# elif selected == 'wanted':
|
|
# profileStr += \
|
|
# _htmlProfileShares(actor, translate,
|
|
# nickname, domainFull,
|
|
# extraJson, 'wanted') + licenseStr
|
|
# end of #timeline
|
|
profileStr += '</div>'
|
|
|
|
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, systemLanguage: str,
|
|
maxLikeCount: int,
|
|
signingPrivateKeyPem: 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(signingPrivateKeyPem,
|
|
True, recentPostsCache,
|
|
maxRecentPosts,
|
|
translate, None,
|
|
baseDir, session, cachedWebfingers,
|
|
personCache,
|
|
nickname, domain, port, item,
|
|
None, True, False,
|
|
httpPrefix, projectVersion, 'inbox',
|
|
YTReplacementDomain,
|
|
showPublishedDateOnly,
|
|
peertubeInstances,
|
|
allowLocalNetworkAccess,
|
|
themeName, systemLanguage,
|
|
maxLikeCount,
|
|
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,
|
|
signingPrivateKeyPem: str) -> str:
|
|
"""Shows following on the profile screen
|
|
"""
|
|
profileStr = ''
|
|
|
|
if authorized and pageNumber:
|
|
if authorized and pageNumber > 1:
|
|
# page up arrow
|
|
profileStr += \
|
|
' <center>\n' + \
|
|
' <a href="' + actor + '/' + feedName + \
|
|
'?page=' + str(pageNumber - 1) + '#buttonheader' + \
|
|
'"><img loading="lazy" class="pageicon" src="/' + \
|
|
'icons/pageup.png" title="' + \
|
|
translate['Page up'] + '" alt="' + \
|
|
translate['Page up'] + '"></a>\n' + \
|
|
' </center>\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(signingPrivateKeyPem,
|
|
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 += \
|
|
' <center>\n' + \
|
|
' <a href="' + actor + '/' + feedName + \
|
|
'?page=' + str(pageNumber + 1) + '#buttonheader' + \
|
|
'"><img loading="lazy" class="pageicon" src="/' + \
|
|
'icons/pagedown.png" title="' + \
|
|
translate['Page down'] + '" alt="' + \
|
|
translate['Page down'] + '"></a>\n' + \
|
|
' </center>\n'
|
|
return profileStr
|
|
|
|
|
|
def _htmlProfileRoles(translate: {}, nickname: str, domain: str,
|
|
rolesList: []) -> str:
|
|
"""Shows roles on the profile screen
|
|
"""
|
|
profileStr = ''
|
|
profileStr += \
|
|
'<div class="roles">\n<div class="roles-inner">\n'
|
|
for role in rolesList:
|
|
if translate.get(role):
|
|
profileStr += '<h3>' + translate[role] + '</h3>\n'
|
|
else:
|
|
profileStr += '<h3>' + role + '</h3>\n'
|
|
profileStr += '</div></div>\n'
|
|
if len(profileStr) == 0:
|
|
profileStr += \
|
|
'<p>@' + nickname + '@' + domain + ' has no roles assigned</p>\n'
|
|
else:
|
|
profileStr = '<div>' + profileStr + '</div>\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 += \
|
|
'<div>' + skill + \
|
|
'<br><div id="myProgress"><div id="myBar" style="width:' + \
|
|
str(level) + '%"></div></div></div>\n<br>\n'
|
|
if len(profileStr) > 0:
|
|
profileStr = '<center><div class="skill-title">' + \
|
|
profileStr + '</div></center>\n'
|
|
return profileStr
|
|
|
|
|
|
def _htmlProfileShares(actor: str, translate: {},
|
|
nickname: str, domain: str, sharesJson: {},
|
|
sharesFileType: str) -> str:
|
|
"""Shows shares on the profile screen
|
|
"""
|
|
profileStr = ''
|
|
for item in sharesJson['orderedItems']:
|
|
profileStr += htmlIndividualShare(domain, item['shareId'],
|
|
actor, item, translate, False, False,
|
|
sharesFileType)
|
|
if len(profileStr) > 0:
|
|
profileStr = '<div class="share-title">' + profileStr + '</div>\n'
|
|
return profileStr
|
|
|
|
|
|
def _grayscaleEnabled(baseDir: str) -> bool:
|
|
"""Is grayscale UI enabled?
|
|
"""
|
|
return os.path.isfile(baseDir + '/accounts/.grayscale')
|
|
|
|
|
|
def _htmlThemesDropdown(baseDir: str, translate: {}) -> str:
|
|
"""Returns the html for theme selection dropdown
|
|
"""
|
|
# Themes section
|
|
themes = getThemesList(baseDir)
|
|
themesDropdown = ' <label class="labels">' + \
|
|
translate['Theme'] + '</label><br>\n'
|
|
grayscale = _grayscaleEnabled(baseDir)
|
|
themesDropdown += \
|
|
editCheckBox(translate['Grayscale'], 'grayscale', grayscale)
|
|
themesDropdown += ' <select id="themeDropdown" ' + \
|
|
'name="themeDropdown" class="theme">'
|
|
for themeName in themes:
|
|
translatedThemeName = themeName
|
|
if translate.get(themeName):
|
|
translatedThemeName = translate[themeName]
|
|
themesDropdown += ' <option value="' + \
|
|
themeName.lower() + '">' + \
|
|
translatedThemeName + '</option>'
|
|
themesDropdown += ' </select><br>'
|
|
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 += \
|
|
editCheckBox(translate['Remove the custom font'],
|
|
'removeCustomFont', False)
|
|
themeName = getConfigParam(baseDir, 'theme')
|
|
themesDropdown = \
|
|
themesDropdown.replace('<option value="' + themeName + '">',
|
|
'<option value="' + themeName +
|
|
'" selected>')
|
|
return themesDropdown
|
|
|
|
|
|
def _htmlEditProfileGraphicDesign(baseDir: str, translate: {}) -> str:
|
|
"""Graphic design section on Edit Profile screen
|
|
"""
|
|
themeFormats = '.zip, .gz'
|
|
|
|
graphicsStr = beginEditSection(translate['Graphic Design'])
|
|
|
|
lowBandwidth = getConfigParam(baseDir, 'lowBandwidth')
|
|
if not lowBandwidth:
|
|
lowBandwidth = False
|
|
graphicsStr += _htmlThemesDropdown(baseDir, translate)
|
|
graphicsStr += \
|
|
' <label class="labels">' + \
|
|
translate['Import Theme'] + '</label>\n'
|
|
graphicsStr += ' <input type="file" id="importTheme" '
|
|
graphicsStr += 'name="submitImportTheme" '
|
|
graphicsStr += 'accept="' + themeFormats + '">\n'
|
|
graphicsStr += \
|
|
' <label class="labels">' + \
|
|
translate['Export Theme'] + '</label><br>\n'
|
|
graphicsStr += \
|
|
' <button type="submit" class="button" ' + \
|
|
'name="submitExportTheme">➤</button><br>\n'
|
|
graphicsStr += \
|
|
editCheckBox(translate['Low Bandwidth'], 'lowBandwidth',
|
|
bool(lowBandwidth))
|
|
|
|
graphicsStr += endEditSection()
|
|
return graphicsStr
|
|
|
|
|
|
def _htmlEditProfileInstance(baseDir: str, translate: {},
|
|
peertubeInstances: [],
|
|
mediaInstanceStr: str,
|
|
blogsInstanceStr: str,
|
|
newsInstanceStr: str) -> (str, str, str, str):
|
|
"""Edit profile instance settings
|
|
"""
|
|
imageFormats = getImageFormats()
|
|
|
|
# Instance details section
|
|
instanceDescription = \
|
|
getConfigParam(baseDir, 'instanceDescription')
|
|
customSubmitText = \
|
|
getConfigParam(baseDir, 'customSubmitText')
|
|
instanceDescriptionShort = \
|
|
getConfigParam(baseDir, 'instanceDescriptionShort')
|
|
instanceTitle = \
|
|
getConfigParam(baseDir, 'instanceTitle')
|
|
|
|
instanceStr = beginEditSection(translate['Instance Settings'])
|
|
|
|
instanceStr += \
|
|
editTextField(translate['Instance Title'],
|
|
'instanceTitle', instanceTitle)
|
|
instanceStr += '<br>\n'
|
|
instanceStr += \
|
|
editTextField(translate['Instance Short Description'],
|
|
'instanceDescriptionShort', instanceDescriptionShort)
|
|
instanceStr += '<br>\n'
|
|
instanceStr += \
|
|
editTextArea(translate['Instance Description'],
|
|
'instanceDescription', instanceDescription, 200,
|
|
'', True)
|
|
instanceStr += \
|
|
editTextField(translate['Custom post submit button text'],
|
|
'customSubmitText', customSubmitText)
|
|
instanceStr += '<br>\n'
|
|
instanceStr += \
|
|
' <label class="labels">' + \
|
|
translate['Instance Logo'] + '</label>' + \
|
|
' <input type="file" id="instanceLogo" name="instanceLogo"' + \
|
|
' accept="' + imageFormats + '"><br>\n' + \
|
|
' <br><label class="labels">' + \
|
|
translate['Security'] + '</label><br>\n'
|
|
|
|
nodeInfoStr = \
|
|
translate['Show numbers of accounts within instance metadata']
|
|
if getConfigParam(baseDir, "showNodeInfoAccounts"):
|
|
instanceStr += \
|
|
editCheckBox(nodeInfoStr, 'showNodeInfoAccounts', True)
|
|
else:
|
|
instanceStr += \
|
|
editCheckBox(nodeInfoStr, 'showNodeInfoAccounts', False)
|
|
|
|
nodeInfoStr = \
|
|
translate['Show version number within instance metadata']
|
|
if getConfigParam(baseDir, "showNodeInfoVersion"):
|
|
instanceStr += \
|
|
editCheckBox(nodeInfoStr, 'showNodeInfoVersion', True)
|
|
else:
|
|
instanceStr += \
|
|
editCheckBox(nodeInfoStr, 'showNodeInfoVersion', False)
|
|
|
|
if getConfigParam(baseDir, "verifyAllSignatures"):
|
|
instanceStr += \
|
|
editCheckBox(translate['Verify all signatures'],
|
|
'verifyallsignatures', True)
|
|
else:
|
|
instanceStr += \
|
|
editCheckBox(translate['Verify all signatures'],
|
|
'verifyallsignatures', False)
|
|
|
|
instanceStr += translate['Enabling broch mode'] + '<br>\n'
|
|
if getConfigParam(baseDir, "brochMode"):
|
|
instanceStr += \
|
|
editCheckBox(translate['Broch mode'], 'brochMode', True)
|
|
else:
|
|
instanceStr += \
|
|
editCheckBox(translate['Broch mode'], 'brochMode', False)
|
|
# Instance type
|
|
instanceStr += \
|
|
' <br><label class="labels">' + \
|
|
translate['Type of instance'] + '</label><br>\n'
|
|
instanceStr += \
|
|
editCheckBox(translate['This is a media instance'],
|
|
'mediaInstance', mediaInstanceStr)
|
|
instanceStr += \
|
|
editCheckBox(translate['This is a blogging instance'],
|
|
'blogsInstance', blogsInstanceStr)
|
|
instanceStr += \
|
|
editCheckBox(translate['This is a news instance'],
|
|
'newsInstance', newsInstanceStr)
|
|
|
|
instanceStr += endEditSection()
|
|
|
|
# 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 = \
|
|
beginEditSection(translate['Role Assignment']) + \
|
|
' <b><label class="labels">' + \
|
|
translate['Moderators'] + '</label></b><br>\n' + \
|
|
' ' + \
|
|
translate['A list of moderator nicknames. One per line.'] + \
|
|
' <textarea id="message" name="moderators" placeholder="' + \
|
|
translate['List of moderator nicknames'] + \
|
|
'..." style="height:200px" spellcheck="false">' + \
|
|
moderators + '</textarea>'
|
|
|
|
# site editors
|
|
editors = ''
|
|
editorsFile = baseDir + '/accounts/editors.txt'
|
|
if os.path.isfile(editorsFile):
|
|
with open(editorsFile, 'r') as f:
|
|
editors = f.read()
|
|
roleAssignStr += \
|
|
' <b><label class="labels">' + \
|
|
translate['Site Editors'] + '</label></b><br>\n' + \
|
|
' ' + \
|
|
translate['A list of editor nicknames. One per line.'] + \
|
|
' <textarea id="message" name="editors" placeholder="" ' + \
|
|
'style="height:200px" spellcheck="false">' + \
|
|
editors + '</textarea>'
|
|
|
|
# counselors
|
|
counselors = ''
|
|
counselorsFile = baseDir + '/accounts/counselors.txt'
|
|
if os.path.isfile(counselorsFile):
|
|
with open(counselorsFile, 'r') as f:
|
|
counselors = f.read()
|
|
roleAssignStr += \
|
|
editTextArea(translate['Counselors'], 'counselors', counselors,
|
|
200, '', False)
|
|
|
|
# artists
|
|
artists = ''
|
|
artistsFile = baseDir + '/accounts/artists.txt'
|
|
if os.path.isfile(artistsFile):
|
|
with open(artistsFile, 'r') as f:
|
|
artists = f.read()
|
|
roleAssignStr += \
|
|
editTextArea(translate['Artists'], 'artists', artists,
|
|
200, '', False)
|
|
roleAssignStr += endEditSection()
|
|
|
|
# Video section
|
|
peertubeStr = beginEditSection(translate['Video Settings'])
|
|
peertubeInstancesStr = ''
|
|
for url in peertubeInstances:
|
|
peertubeInstancesStr += url + '\n'
|
|
peertubeStr += \
|
|
editTextArea(translate['Peertube Instances'], 'ptInstances',
|
|
peertubeInstancesStr, 200, '', False)
|
|
peertubeStr += \
|
|
' <br>\n'
|
|
YTReplacementDomain = getConfigParam(baseDir, "youtubedomain")
|
|
if not YTReplacementDomain:
|
|
YTReplacementDomain = ''
|
|
peertubeStr += \
|
|
editTextField(translate['YouTube Replacement Domain'],
|
|
'ytdomain', YTReplacementDomain)
|
|
peertubeStr += endEditSection()
|
|
|
|
libretranslateUrl = getConfigParam(baseDir, 'libretranslateUrl')
|
|
libretranslateApiKey = getConfigParam(baseDir, 'libretranslateApiKey')
|
|
libretranslateStr = \
|
|
_htmlEditProfileLibreTranslate(translate,
|
|
libretranslateUrl,
|
|
libretranslateApiKey)
|
|
|
|
return instanceStr, roleAssignStr, peertubeStr, libretranslateStr
|
|
|
|
|
|
def _htmlEditProfileDangerZone(translate: {}) -> str:
|
|
"""danger zone section of Edit Profile screen
|
|
"""
|
|
editProfileForm = beginEditSection(translate['Danger Zone'])
|
|
|
|
editProfileForm += \
|
|
' <b><label class="labels">' + \
|
|
translate['Danger Zone'] + '</label></b><br>\n'
|
|
|
|
editProfileForm += \
|
|
editCheckBox(translate['Deactivate this account'],
|
|
'deactivateThisAccount', False)
|
|
|
|
editProfileForm += endEditSection()
|
|
return editProfileForm
|
|
|
|
|
|
def _htmlEditProfileSkills(baseDir: str, nickname: str, domain: str,
|
|
translate: {}) -> str:
|
|
"""skills section of Edit Profile screen
|
|
"""
|
|
skills = getSkills(baseDir, nickname, domain)
|
|
skillsStr = ''
|
|
skillCtr = 1
|
|
if skills:
|
|
for skillDesc, skillValue in skills.items():
|
|
if isFiltered(baseDir, nickname, domain, skillDesc):
|
|
continue
|
|
skillsStr += \
|
|
'<p><input type="text" placeholder="' + translate['Skill'] + \
|
|
' ' + str(skillCtr) + '" name="skillName' + str(skillCtr) + \
|
|
'" value="' + skillDesc + '" style="width:40%">' + \
|
|
'<input type="range" min="1" max="100" ' + \
|
|
'class="slider" name="skillValue' + \
|
|
str(skillCtr) + '" value="' + str(skillValue) + '"></p>'
|
|
skillCtr += 1
|
|
|
|
skillsStr += \
|
|
'<p><input type="text" placeholder="Skill ' + str(skillCtr) + \
|
|
'" name="skillName' + str(skillCtr) + \
|
|
'" value="" style="width:40%">' + \
|
|
'<input type="range" min="1" max="100" ' + \
|
|
'class="slider" name="skillValue' + \
|
|
str(skillCtr) + '" value="50"></p>' + endEditSection()
|
|
|
|
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 = \
|
|
beginEditSection(translate['Skills']) + \
|
|
' <b><label class="labels">' + \
|
|
translate['Skills'] + '</label></b><br>\n' + \
|
|
' <label class="labels">' + \
|
|
translate[idx] + '</label>\n' + skillsStr
|
|
return editProfileForm
|
|
|
|
|
|
def _htmlEditProfileGitProjects(baseDir: str, nickname: str, domain: str,
|
|
translate: {}) -> str:
|
|
"""git projects section of edit profile screen
|
|
"""
|
|
gitProjectsStr = ''
|
|
gitProjectsFilename = \
|
|
acctDir(baseDir, nickname, domain) + '/gitprojects.txt'
|
|
if os.path.isfile(gitProjectsFilename):
|
|
with open(gitProjectsFilename, 'r') as gitProjectsFile:
|
|
gitProjectsStr = gitProjectsFile.read()
|
|
|
|
editProfileForm = beginEditSection(translate['Git Projects'])
|
|
idx = 'List of project names that you wish to receive git patches for'
|
|
editProfileForm += \
|
|
editTextArea(translate[idx], 'gitProjects', gitProjectsStr,
|
|
100, '', False)
|
|
editProfileForm += endEditSection()
|
|
return editProfileForm
|
|
|
|
|
|
def _htmlEditProfileSharedItems(baseDir: str, nickname: str, domain: str,
|
|
translate: {}) -> str:
|
|
"""shared items section of edit profile screen
|
|
"""
|
|
sharedItemsStr = ''
|
|
sharedItemsFederatedDomainsStr = \
|
|
getConfigParam(baseDir, 'sharedItemsFederatedDomains')
|
|
if sharedItemsFederatedDomainsStr:
|
|
sharedItemsFederatedDomainsList = \
|
|
sharedItemsFederatedDomainsStr.split(',')
|
|
for sharedFederatedDomain in sharedItemsFederatedDomainsList:
|
|
sharedItemsStr += sharedFederatedDomain.strip() + '\n'
|
|
|
|
editProfileForm = beginEditSection(translate['Shares'])
|
|
idx = 'List of domains which can access the shared items catalog'
|
|
editProfileForm += \
|
|
editTextArea(translate[idx], 'shareDomainList',
|
|
sharedItemsStr, 200, '', False)
|
|
editProfileForm += endEditSection()
|
|
return editProfileForm
|
|
|
|
|
|
def _htmlEditProfileFiltering(baseDir: str, nickname: str, domain: str,
|
|
userAgentsBlocked: str, translate: {}) -> str:
|
|
"""Filtering and blocking section of edit profile screen
|
|
"""
|
|
filterStr = ''
|
|
filterFilename = \
|
|
acctDir(baseDir, nickname, domain) + '/filters.txt'
|
|
if os.path.isfile(filterFilename):
|
|
with open(filterFilename, 'r') as filterfile:
|
|
filterStr = filterfile.read()
|
|
|
|
switchStr = ''
|
|
switchFilename = \
|
|
acctDir(baseDir, nickname, domain) + '/replacewords.txt'
|
|
if os.path.isfile(switchFilename):
|
|
with open(switchFilename, 'r') as switchfile:
|
|
switchStr = switchfile.read()
|
|
|
|
autoTags = ''
|
|
autoTagsFilename = \
|
|
acctDir(baseDir, nickname, domain) + '/autotags.txt'
|
|
if os.path.isfile(autoTagsFilename):
|
|
with open(autoTagsFilename, 'r') as autoTagsFile:
|
|
autoTags = autoTagsFile.read()
|
|
|
|
autoCW = ''
|
|
autoCWFilename = \
|
|
acctDir(baseDir, nickname, domain) + '/autocw.txt'
|
|
if os.path.isfile(autoCWFilename):
|
|
with open(autoCWFilename, 'r') as autoCWFile:
|
|
autoCW = autoCWFile.read()
|
|
|
|
blockedStr = ''
|
|
blockedFilename = \
|
|
acctDir(baseDir, nickname, domain) + '/blocking.txt'
|
|
if os.path.isfile(blockedFilename):
|
|
with open(blockedFilename, 'r') as blockedfile:
|
|
blockedStr = blockedfile.read()
|
|
|
|
dmAllowedInstancesStr = ''
|
|
dmAllowedInstancesFilename = \
|
|
acctDir(baseDir, nickname, domain) + '/dmAllowedInstances.txt'
|
|
if os.path.isfile(dmAllowedInstancesFilename):
|
|
with open(dmAllowedInstancesFilename, 'r') as dmAllowedInstancesFile:
|
|
dmAllowedInstancesStr = dmAllowedInstancesFile.read()
|
|
|
|
allowedInstancesStr = ''
|
|
allowedInstancesFilename = \
|
|
acctDir(baseDir, nickname, domain) + '/allowedinstances.txt'
|
|
if os.path.isfile(allowedInstancesFilename):
|
|
with open(allowedInstancesFilename, 'r') as allowedInstancesFile:
|
|
allowedInstancesStr = allowedInstancesFile.read()
|
|
|
|
editProfileForm = beginEditSection(translate['Filtering and Blocking'])
|
|
|
|
editProfileForm += \
|
|
'<label class="labels">' + \
|
|
translate['City for spoofed GPS image metadata'] + \
|
|
'</label><br>\n'
|
|
|
|
city = ''
|
|
cityFilename = acctDir(baseDir, 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 += ' <select id="cityDropdown" ' + \
|
|
'name="cityDropdown" class="theme">\n'
|
|
city = city.lower()
|
|
for cityName in cities:
|
|
if ':' not in cityName:
|
|
continue
|
|
citySelected = ''
|
|
cityName = cityName.split(':')[0]
|
|
cityName = cityName.lower()
|
|
if city:
|
|
if city in cityName:
|
|
citySelected = ' selected'
|
|
editProfileForm += \
|
|
' <option value="' + cityName + \
|
|
'"' + citySelected.title() + '>' + \
|
|
cityName + '</option>\n'
|
|
editProfileForm += ' </select><br>\n'
|
|
|
|
editProfileForm += \
|
|
' <b><label class="labels">' + \
|
|
translate['Filtered words'] + '</label></b>\n' + \
|
|
' <br><label class="labels">' + \
|
|
translate['One per line'] + '</label>\n' + \
|
|
' <textarea id="message" ' + \
|
|
'name="filteredWords" style="height:200px" spellcheck="false">' + \
|
|
filterStr + '</textarea>\n' + \
|
|
' <br><b><label class="labels">' + \
|
|
translate['Word Replacements'] + '</label></b>\n' + \
|
|
' <br><label class="labels">A -> B</label>\n' + \
|
|
' <textarea id="message" name="switchWords" ' + \
|
|
'style="height:200px" spellcheck="false">' + \
|
|
switchStr + '</textarea>\n' + \
|
|
' <br><b><label class="labels">' + \
|
|
translate['Autogenerated Hashtags'] + '</label></b>\n' + \
|
|
' <br><label class="labels">A -> #B</label>\n' + \
|
|
' <textarea id="message" name="autoTags" ' + \
|
|
'style="height:200px" spellcheck="false">' + \
|
|
autoTags + '</textarea>\n' + \
|
|
' <br><b><label class="labels">' + \
|
|
translate['Autogenerated Content Warnings'] + '</label></b>\n' + \
|
|
' <br><label class="labels">A -> B</label>\n' + \
|
|
' <textarea id="message" name="autoCW" ' + \
|
|
'style="height:200px" spellcheck="true">' + autoCW + '</textarea>\n'
|
|
|
|
idx = 'Blocked accounts, one per line, in the form ' + \
|
|
'nickname@domain or *@blockeddomain'
|
|
editProfileForm += \
|
|
editTextArea(translate['Blocked accounts'], 'blocked', blockedStr,
|
|
200, '', False)
|
|
|
|
idx = 'Direct messages are always allowed from these instances.'
|
|
editProfileForm += \
|
|
editTextArea(translate['Direct Message permitted instances'],
|
|
'dmAllowedInstances', dmAllowedInstancesStr,
|
|
200, '', False)
|
|
|
|
idx = 'Federate only with a defined set of instances. ' + \
|
|
'One domain name per line.'
|
|
editProfileForm += \
|
|
' <br><b><label class="labels">' + \
|
|
translate['Federation list'] + '</label></b>\n' + \
|
|
' <br><label class="labels">' + \
|
|
translate[idx] + '</label>\n' + \
|
|
' <textarea id="message" name="allowedInstances" ' + \
|
|
'style="height:200px" spellcheck="false">' + \
|
|
allowedInstancesStr + '</textarea>\n'
|
|
|
|
userAgentsBlockedStr = ''
|
|
for ua in userAgentsBlocked:
|
|
if userAgentsBlockedStr:
|
|
userAgentsBlockedStr += '\n'
|
|
userAgentsBlockedStr += ua
|
|
editProfileForm += \
|
|
editTextArea(translate['Blocked User Agents'],
|
|
'userAgentsBlockedStr', userAgentsBlockedStr,
|
|
200, '', False)
|
|
|
|
editProfileForm += endEditSection()
|
|
return editProfileForm
|
|
|
|
|
|
def _htmlEditProfileChangePassword(translate: {}) -> str:
|
|
"""Change password section of edit profile screen
|
|
"""
|
|
editProfileForm = \
|
|
beginEditSection(translate['Change Password']) + \
|
|
'<label class="labels">' + translate['Change Password'] + \
|
|
'</label><br>\n' + \
|
|
' <input type="password" name="password" ' + \
|
|
'value=""><br>\n' + \
|
|
'<label class="labels">' + translate['Confirm Password'] + \
|
|
'</label><br>\n' + \
|
|
' <input type="password" name="passwordconfirm" value="">\n' + \
|
|
endEditSection()
|
|
return editProfileForm
|
|
|
|
|
|
def _htmlEditProfileLibreTranslate(translate: {},
|
|
libretranslateUrl: str,
|
|
libretranslateApiKey: str) -> str:
|
|
"""Change automatic translation settings
|
|
"""
|
|
editProfileForm = beginEditSection('LibreTranslate')
|
|
|
|
editProfileForm += \
|
|
editTextField('URL', 'libretranslateUrl', libretranslateUrl,
|
|
'http://0.0.0.0:5000')
|
|
editProfileForm += \
|
|
editTextField('API Key', 'libretranslateApiKey', libretranslateApiKey)
|
|
|
|
editProfileForm += endEditSection()
|
|
return editProfileForm
|
|
|
|
|
|
def _htmlEditProfileBackground(newsInstance: bool, translate: {}) -> str:
|
|
"""Background images section of edit profile screen
|
|
"""
|
|
idx = 'The files attached below should be no larger than ' + \
|
|
'10MB in total uploaded at once.'
|
|
editProfileForm = \
|
|
beginEditSection(translate['Background Images']) + \
|
|
' <label class="labels">' + translate[idx] + '</label><br><br>\n'
|
|
|
|
if not newsInstance:
|
|
imageFormats = getImageFormats()
|
|
editProfileForm += \
|
|
' <label class="labels">' + \
|
|
translate['Background image'] + '</label>\n' + \
|
|
' <input type="file" id="image" name="image"' + \
|
|
' accept="' + imageFormats + '">\n' + \
|
|
' <br><label class="labels">' + \
|
|
translate['Timeline banner image'] + '</label>\n' + \
|
|
' <input type="file" id="banner" name="banner"' + \
|
|
' accept="' + imageFormats + '">\n' + \
|
|
' <br><label class="labels">' + \
|
|
translate['Search banner image'] + '</label>\n' + \
|
|
' <input type="file" id="search_banner" ' + \
|
|
'name="search_banner"' + \
|
|
' accept="' + imageFormats + '">\n' + \
|
|
' <br><label class="labels">' + \
|
|
translate['Left column image'] + '</label>\n' + \
|
|
' <input type="file" id="left_col_image" ' + \
|
|
'name="left_col_image"' + \
|
|
' accept="' + imageFormats + '">\n' + \
|
|
' <br><label class="labels">' + \
|
|
translate['Right column image'] + '</label>\n' + \
|
|
' <input type="file" id="right_col_image" ' + \
|
|
'name="right_col_image"' + \
|
|
' accept="' + imageFormats + '">\n'
|
|
|
|
editProfileForm += endEditSection()
|
|
return editProfileForm
|
|
|
|
|
|
def _htmlEditProfileContactInfo(nickname: str,
|
|
emailAddress: str,
|
|
xmppAddress: str,
|
|
matrixAddress: str,
|
|
ssbAddress: str,
|
|
toxAddress: str,
|
|
briarAddress: str,
|
|
jamiAddress: str,
|
|
cwtchAddress: str,
|
|
PGPfingerprint: str,
|
|
PGPpubKey: str,
|
|
translate: {}) -> str:
|
|
"""Contact Information section of edit profile screen
|
|
"""
|
|
editProfileForm = beginEditSection(translate['Contact Details'])
|
|
|
|
editProfileForm += editTextField(translate['Email'],
|
|
'email', emailAddress)
|
|
editProfileForm += editTextField(translate['XMPP'],
|
|
'xmppAddress', xmppAddress)
|
|
editProfileForm += editTextField(translate['Matrix'],
|
|
'matrixAddress', matrixAddress)
|
|
editProfileForm += editTextField('SSB', 'ssbAddress', ssbAddress)
|
|
editProfileForm += editTextField('Tox', 'toxAddress', toxAddress)
|
|
editProfileForm += editTextField('Briar', 'briarAddress', briarAddress)
|
|
editProfileForm += editTextField('Jami', 'jamiAddress', jamiAddress)
|
|
editProfileForm += editTextField('Cwtch', 'cwtchAddress', cwtchAddress)
|
|
editProfileForm += editTextField(translate['PGP Fingerprint'],
|
|
'openpgp', PGPfingerprint)
|
|
editProfileForm += \
|
|
editTextArea(translate['PGP'], 'pgp', PGPpubKey, 600,
|
|
'-----BEGIN PGP PUBLIC KEY BLOCK-----', False)
|
|
editProfileForm += \
|
|
'<a href="/users/' + nickname + \
|
|
'/followingaccounts"><label class="labels">' + \
|
|
translate['Following'] + '</label></a><br>\n'
|
|
|
|
editProfileForm += endEditSection()
|
|
return editProfileForm
|
|
|
|
|
|
def _htmlEditProfileOptions(isAdmin: bool,
|
|
manuallyApprovesFollowers: str,
|
|
isBot: str, isGroup: str,
|
|
followDMs: str, removeTwitter: str,
|
|
notifyLikes: str, hideLikeButton: str,
|
|
translate: {}) -> str:
|
|
"""option checkboxes section of edit profile screen
|
|
"""
|
|
editProfileForm = ' <div class="container">\n'
|
|
editProfileForm += \
|
|
editCheckBox(translate['Approve follower requests'],
|
|
'approveFollowers', manuallyApprovesFollowers)
|
|
editProfileForm += \
|
|
editCheckBox(translate['This is a bot account'],
|
|
'isBot', isBot)
|
|
if isAdmin:
|
|
editProfileForm += \
|
|
editCheckBox(translate['This is a group account'],
|
|
'isGroup', isGroup)
|
|
editProfileForm += \
|
|
editCheckBox(translate['Only people I follow can send me DMs'],
|
|
'followDMs', followDMs)
|
|
editProfileForm += \
|
|
editCheckBox(translate['Remove Twitter posts'],
|
|
'removeTwitter', removeTwitter)
|
|
editProfileForm += \
|
|
editCheckBox(translate['Notify when posts are liked'],
|
|
'notifyLikes', notifyLikes)
|
|
editProfileForm += \
|
|
editCheckBox(translate["Don't show the Like button"],
|
|
'hideLikeButton', hideLikeButton)
|
|
editProfileForm += ' </div>\n'
|
|
return editProfileForm
|
|
|
|
|
|
def _getSupportedLanguagesSorted(baseDir: str) -> str:
|
|
"""Returns a list of supported languages
|
|
"""
|
|
langList = getSupportedLanguages(baseDir)
|
|
if not langList:
|
|
return ''
|
|
langList.sort()
|
|
languagesStr = ''
|
|
for lang in langList:
|
|
if languagesStr:
|
|
languagesStr += ' / ' + lang
|
|
else:
|
|
languagesStr = lang
|
|
return languagesStr
|
|
|
|
|
|
def _htmlEditProfileMain(baseDir: str, displayNickname: str, bioStr: str,
|
|
movedTo: str, donateUrl: str, websiteUrl: str,
|
|
blogAddress: str, actorJson: {},
|
|
translate: {}) -> str:
|
|
"""main info on edit profile screen
|
|
"""
|
|
imageFormats = getImageFormats()
|
|
|
|
editProfileForm = ' <div class="container">\n'
|
|
|
|
editProfileForm += \
|
|
editTextField(translate['Nickname'], 'displayNickname',
|
|
displayNickname)
|
|
|
|
editProfileForm += \
|
|
editTextArea(translate['Your bio'], 'bio', bioStr, 200, '', True)
|
|
|
|
editProfileForm += \
|
|
' <label class="labels">' + translate['Avatar image'] + \
|
|
'</label>\n' + \
|
|
' <input type="file" id="avatar" name="avatar"' + \
|
|
' accept="' + imageFormats + '">\n'
|
|
|
|
occupationName = ''
|
|
if actorJson.get('hasOccupation'):
|
|
occupationName = getOccupationName(actorJson)
|
|
|
|
editProfileForm += \
|
|
editTextField(translate['Occupation'], 'occupationName',
|
|
occupationName)
|
|
|
|
alsoKnownAsStr = ''
|
|
if actorJson.get('alsoKnownAs'):
|
|
alsoKnownAs = actorJson['alsoKnownAs']
|
|
ctr = 0
|
|
for altActor in alsoKnownAs:
|
|
if ctr > 0:
|
|
alsoKnownAsStr += ', '
|
|
ctr += 1
|
|
alsoKnownAsStr += altActor
|
|
|
|
editProfileForm += \
|
|
editTextField(translate['Other accounts'], 'alsoKnownAs',
|
|
alsoKnownAsStr, 'https://...')
|
|
|
|
editProfileForm += \
|
|
editTextField(translate['Moved to new account address'], 'movedTo',
|
|
movedTo, 'https://...')
|
|
|
|
editProfileForm += \
|
|
editTextField(translate['Donations link'], 'donateUrl',
|
|
donateUrl, 'https://...')
|
|
|
|
editProfileForm += \
|
|
editTextField(translate['Website'], 'websiteUrl',
|
|
websiteUrl, 'https://...')
|
|
|
|
editProfileForm += \
|
|
editTextField('Blog', 'blogAddress', blogAddress, 'https://...')
|
|
|
|
languagesListStr = _getSupportedLanguagesSorted(baseDir)
|
|
showLanguages = getActorLanguages(actorJson)
|
|
editProfileForm += \
|
|
editTextField(translate['Languages'], 'showLanguages',
|
|
showLanguages, languagesListStr)
|
|
|
|
editProfileForm += ' </div>\n'
|
|
return editProfileForm
|
|
|
|
|
|
def _htmlEditProfileTopBanner(baseDir: str,
|
|
nickname: str, domain: str, domainFull: str,
|
|
defaultTimeline: str, bannerFile: str,
|
|
path: str, accessKeys: {}, translate: {}) -> str:
|
|
"""top banner on edit profile screen
|
|
"""
|
|
editProfileForm = \
|
|
'<a href="/users/' + nickname + '/' + defaultTimeline + '">' + \
|
|
'<img loading="lazy" class="timeline-banner" src="' + \
|
|
'/users/' + nickname + '/' + bannerFile + '" alt="" /></a>\n'
|
|
|
|
editProfileForm += \
|
|
'<form enctype="multipart/form-data" method="POST" ' + \
|
|
'accept-charset="UTF-8" action="' + path + '/profiledata">\n'
|
|
editProfileForm += ' <div class="vertical-center">\n'
|
|
editProfileForm += \
|
|
' <h1>' + translate['Profile for'] + \
|
|
' ' + nickname + '@' + domainFull + '</h1>'
|
|
editProfileForm += ' <div class="container">\n'
|
|
editProfileForm += \
|
|
' <center>\n' + \
|
|
' <input type="submit" name="submitProfile" ' + \
|
|
'accesskey="' + accessKeys['submitButton'] + '" ' + \
|
|
'value="' + translate['Submit'] + '">\n' + \
|
|
' </center>\n'
|
|
editProfileForm += ' </div>\n'
|
|
|
|
if scheduledPostsExist(baseDir, nickname, domain):
|
|
editProfileForm += ' <div class="container">\n'
|
|
editProfileForm += \
|
|
editCheckBox(translate['Remove scheduled posts'],
|
|
'removeScheduledPosts', False)
|
|
editProfileForm += ' </div>\n'
|
|
return editProfileForm
|
|
|
|
|
|
def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str,
|
|
domain: str, port: int, httpPrefix: str,
|
|
defaultTimeline: str, theme: str,
|
|
peertubeInstances: [],
|
|
textModeBanner: str, city: str,
|
|
userAgentsBlocked: str,
|
|
accessKeys: {}) -> str:
|
|
"""Shows the edit profile screen
|
|
"""
|
|
path = path.replace('/inbox', '').replace('/outbox', '')
|
|
path = path.replace('/shares', '').replace('/wanted', '')
|
|
nickname = getNicknameFromActor(path)
|
|
if not nickname:
|
|
return ''
|
|
domainFull = getFullDomain(domain, port)
|
|
|
|
actorFilename = acctDir(baseDir, 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)
|
|
|
|
displayNickname = nickname
|
|
isBot = isGroup = followDMs = removeTwitter = ''
|
|
notifyLikes = hideLikeButton = mediaInstanceStr = ''
|
|
blogsInstanceStr = newsInstanceStr = movedTo = ''
|
|
bioStr = donateUrl = websiteUrl = emailAddress = PGPpubKey = ''
|
|
PGPfingerprint = xmppAddress = matrixAddress = ''
|
|
ssbAddress = blogAddress = toxAddress = jamiAddress = ''
|
|
cwtchAddress = briarAddress = manuallyApprovesFollowers = ''
|
|
|
|
actorJson = loadJson(actorFilename)
|
|
if actorJson:
|
|
if actorJson.get('movedTo'):
|
|
movedTo = actorJson['movedTo']
|
|
donateUrl = getDonationUrl(actorJson)
|
|
websiteUrl = getWebsite(actorJson, translate)
|
|
xmppAddress = getXmppAddress(actorJson)
|
|
matrixAddress = getMatrixAddress(actorJson)
|
|
ssbAddress = getSSBAddress(actorJson)
|
|
blogAddress = getBlogAddress(actorJson)
|
|
toxAddress = getToxAddress(actorJson)
|
|
briarAddress = getBriarAddress(actorJson)
|
|
jamiAddress = getJamiAddress(actorJson)
|
|
cwtchAddress = getCwtchAddress(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('<p>', '').replace('</p>', '')
|
|
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 = ''
|
|
accountDir = acctDir(baseDir, nickname, domain)
|
|
if os.path.isfile(accountDir + '/.followDMs'):
|
|
followDMs = 'checked'
|
|
if os.path.isfile(accountDir + '/.removeTwitter'):
|
|
removeTwitter = 'checked'
|
|
if os.path.isfile(accountDir + '/.notifyLikes'):
|
|
notifyLikes = 'checked'
|
|
if os.path.isfile(accountDir + '/.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 = ''
|
|
|
|
cssFilename = baseDir + '/epicyon-profile.css'
|
|
if os.path.isfile(baseDir + '/epicyon.css'):
|
|
cssFilename = baseDir + '/epicyon.css'
|
|
|
|
instanceStr = ''
|
|
roleAssignStr = ''
|
|
peertubeStr = ''
|
|
libretranslateStr = ''
|
|
graphicsStr = ''
|
|
sharesFederationStr = ''
|
|
|
|
adminNickname = getConfigParam(baseDir, 'admin')
|
|
|
|
if isArtist(baseDir, nickname) or \
|
|
path.startswith('/users/' + str(adminNickname) + '/'):
|
|
graphicsStr = _htmlEditProfileGraphicDesign(baseDir, translate)
|
|
|
|
isAdmin = False
|
|
if adminNickname:
|
|
if path.startswith('/users/' + adminNickname + '/'):
|
|
isAdmin = True
|
|
# shared items section
|
|
sharesFederationStr = \
|
|
_htmlEditProfileSharedItems(baseDir, nickname,
|
|
domain, translate)
|
|
instanceStr, roleAssignStr, peertubeStr, libretranslateStr = \
|
|
_htmlEditProfileInstance(baseDir, translate,
|
|
peertubeInstances,
|
|
mediaInstanceStr,
|
|
blogsInstanceStr,
|
|
newsInstanceStr)
|
|
|
|
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 += \
|
|
_htmlEditProfileTopBanner(baseDir, nickname, domain, domainFull,
|
|
defaultTimeline, bannerFile,
|
|
path, accessKeys, translate)
|
|
|
|
# main info
|
|
editProfileForm += \
|
|
_htmlEditProfileMain(baseDir, displayNickname, bioStr,
|
|
movedTo, donateUrl, websiteUrl,
|
|
blogAddress, actorJson, translate)
|
|
|
|
# Option checkboxes
|
|
editProfileForm += \
|
|
_htmlEditProfileOptions(isAdmin, manuallyApprovesFollowers,
|
|
isBot, isGroup, followDMs, removeTwitter,
|
|
notifyLikes, hideLikeButton, translate)
|
|
|
|
# Contact information
|
|
editProfileForm += \
|
|
_htmlEditProfileContactInfo(nickname, emailAddress,
|
|
xmppAddress, matrixAddress,
|
|
ssbAddress, toxAddress,
|
|
briarAddress, jamiAddress,
|
|
cwtchAddress, PGPfingerprint,
|
|
PGPpubKey, translate)
|
|
|
|
# Customize images and banners
|
|
editProfileForm += _htmlEditProfileBackground(newsInstance, translate)
|
|
|
|
# Change password
|
|
editProfileForm += _htmlEditProfileChangePassword(translate)
|
|
|
|
# automatic translations
|
|
editProfileForm += libretranslateStr
|
|
|
|
# Filtering and blocking section
|
|
editProfileForm += \
|
|
_htmlEditProfileFiltering(baseDir, nickname, domain,
|
|
userAgentsBlocked, translate)
|
|
|
|
# git projects section
|
|
editProfileForm += \
|
|
_htmlEditProfileGitProjects(baseDir, nickname, domain, translate)
|
|
|
|
# Skills section
|
|
editProfileForm += \
|
|
_htmlEditProfileSkills(baseDir, nickname, domain, translate)
|
|
|
|
editProfileForm += roleAssignStr + peertubeStr + graphicsStr
|
|
editProfileForm += sharesFederationStr + instanceStr
|
|
|
|
# danger zone section
|
|
editProfileForm += _htmlEditProfileDangerZone(translate)
|
|
|
|
editProfileForm += ' <div class="container">\n'
|
|
editProfileForm += \
|
|
' <center>\n' + \
|
|
' <input type="submit" name="submitProfile" value="' + \
|
|
translate['Submit'] + '">\n' + \
|
|
' </center>\n'
|
|
editProfileForm += ' </div>\n'
|
|
|
|
editProfileForm += ' </div>\n'
|
|
editProfileForm += '</form>\n'
|
|
editProfileForm += htmlFooter()
|
|
return editProfileForm
|
|
|
|
|
|
def _individualFollowAsHtml(signingPrivateKeyPem: str,
|
|
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, False,
|
|
signingPrivateKeyPem)
|
|
|
|
(inboxUrl, pubKeyId, pubKey,
|
|
fromPersonId, sharedInbox,
|
|
avatarUrl2, displayName) = getPersonBox(signingPrivateKeyPem,
|
|
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 += \
|
|
'<a href="/users/' + actorNickname + \
|
|
'?options=' + followUrl + \
|
|
';1;' + avatarUrl + '"><button class="buttonunfollow">' + \
|
|
translate['Block'] + '</button></a>\n'
|
|
elif b == 'unfollow':
|
|
buttonsStr += \
|
|
'<a href="/users/' + actorNickname + \
|
|
'?options=' + followUrl + \
|
|
';1;' + avatarUrl + '"><button class="buttonunfollow">' + \
|
|
translate['Unfollow'] + '</button></a>\n'
|
|
|
|
resultStr = '<div class="container">\n'
|
|
resultStr += \
|
|
'<a href="/users/' + actorNickname + '?options=' + \
|
|
followUrl + ';1;' + avatarUrl + '">\n'
|
|
resultStr += '<p><img loading="lazy" src="' + avatarUrl + '" alt=" ">'
|
|
resultStr += titleStr + '</a>' + buttonsStr + '</p>\n'
|
|
resultStr += '</div>\n'
|
|
return resultStr
|