epicyon/webapp_profile.py

2240 lines
89 KiB
Python
Raw Normal View History

2020-11-09 22:44:03 +00:00
__filename__ = "webapp_profile.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
2021-01-26 10:07:42 +00:00
__version__ = "1.2.0"
2020-11-09 22:44:03 +00:00
__maintainer__ = "Bob Mottram"
2021-09-10 16:14:50 +00:00
__email__ = "bob@libreserver.org"
2020-11-09 22:44:03 +00:00
__status__ = "Production"
2021-06-15 15:08:12 +00:00
__module_group__ = "Web Interface"
2020-11-09 22:44:03 +00:00
import os
from pprint import pprint
2021-09-21 17:20:45 +00:00
from utils import isGroupAccount
from utils import hasObjectDict
2021-05-16 15:10:39 +00:00
from utils import getOccupationName
from utils import getLockedAccount
2020-12-16 11:19:16 +00:00
from utils import getFullDomain
from utils import isArtist
from utils import isDormant
2020-11-09 22:44:03 +00:00
from utils import getNicknameFromActor
from utils import getDomainFromActor
from utils import isSystemAccount
from utils import removeHtml
from utils import loadJson
from utils import getConfigParam
2020-11-21 11:21:05 +00:00
from utils import getImageFormats
2021-07-13 21:59:53 +00:00
from utils import acctDir
2021-08-08 11:16:18 +00:00
from utils import getSupportedLanguages
2021-08-14 11:13:39 +00:00
from utils import localActorUrl
from utils import getReplyIntervalHours
from languages import getActorLanguages
2020-11-09 22:44:03 +00:00
from skills import getSkills
from theme import getThemesList
from person import personBoxJson
2021-06-03 19:46:35 +00:00
from person import getActorJson
2021-06-25 14:33:16 +00:00
from person import getPersonAvatarUrl
2020-11-09 22:44:03 +00:00
from webfinger import webfingerHandle
from posts import parseUserFeed
from posts import getPersonBox
from posts import isCreateInsideAnnounce
2020-11-09 22:44:03 +00:00
from donate import getDonationUrl
2021-08-12 20:40:23 +00:00
from donate import getWebsite
2020-11-09 22:44:03 +00:00
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
2020-12-24 16:49:57 +00:00
from briar import getBriarAddress
2020-11-29 12:50:41 +00:00
from jami import getJamiAddress
2021-06-27 11:48:03 +00:00
from cwtch import getCwtchAddress
2020-12-19 10:32:48 +00:00
from filters import isFiltered
from follow import isFollowerOfPerson
from webapp_frontscreen import htmlFrontScreen
2021-02-05 17:05:53 +00:00
from webapp_utils import htmlKeyboardNavigation
2021-02-06 10:35:47 +00:00
from webapp_utils import htmlHideFromScreenReader
2020-11-09 22:44:03 +00:00
from webapp_utils import scheduledPostsExist
2020-11-12 17:05:38 +00:00
from webapp_utils import htmlHeaderWithExternalStyle
from webapp_utils import htmlHeaderWithPersonMarkup
2020-11-09 22:44:03 +00:00
from webapp_utils import htmlFooter
from webapp_utils import addEmojiToDisplayName
from webapp_utils import getBannerFile
from webapp_utils import htmlPostSeparator
2021-07-22 16:58:59 +00:00
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
2021-06-26 11:16:41 +00:00
from blog import getBlogAddress
2020-11-09 22:44:03 +00:00
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,
2021-09-18 17:08:14 +00:00
twitterReplacementDomain: str,
showPublishedDateOnly: bool,
2020-12-23 23:59:49 +00:00
defaultTimeline: str,
peertubeInstances: [],
2021-03-09 19:52:10 +00:00
allowLocalNetworkAccess: bool,
2021-04-23 12:04:42 +00:00
themeName: str,
accessKeys: {},
systemLanguage: str,
maxLikeCount: int,
signingPrivateKeyPem: str) -> str:
2020-11-09 22:44:03 +00:00
"""Show a profile page after a search for a fediverse address
"""
2021-06-03 19:46:35 +00:00
http = False
gnunet = False
if httpPrefix == 'http':
http = True
elif httpPrefix == 'gnunet':
gnunet = True
profileJson, asHeader = \
getActorJson(domain, profileHandle, http, gnunet, debug, False,
signingPrivateKeyPem)
2021-06-03 19:46:35 +00:00
if not profileJson:
2020-11-09 22:44:03 +00:00
return None
2021-06-03 19:46:35 +00:00
personUrl = profileJson['id']
searchDomain, searchPort = getDomainFromActor(personUrl)
if not searchDomain:
return None
2021-06-03 19:46:35 +00:00
searchNickname = getNicknameFromActor(personUrl)
if not searchNickname:
return None
2020-12-16 11:19:16 +00:00
searchDomainFull = getFullDomain(searchDomain, searchPort)
2020-11-09 22:44:03 +00:00
profileStr = ''
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
2021-09-21 18:57:12 +00:00
isGroup = False
if profileJson.get('type'):
if profileJson['type'] == 'Group':
isGroup = True
2020-11-12 17:05:38 +00:00
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']
2021-08-20 13:45:42 +00:00
if '"' in movedTo:
movedTo = movedTo.split('"')[1]
displayName += ''
followsYou = \
isFollowerOfPerson(baseDir,
nickname, domain,
searchNickname,
searchDomainFull)
2020-11-12 17:05:38 +00:00
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:
2020-11-09 22:44:03 +00:00
profileDescriptionShort = ''
2020-11-12 17:05:38 +00:00
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)
2020-11-12 22:41:41 +00:00
2020-11-12 23:44:16 +00:00
imageUrl = ''
if profileJson.get('image'):
if profileJson['image'].get('url'):
imageUrl = profileJson['image']['url']
2021-01-22 20:48:52 +00:00
alsoKnownAs = None
if profileJson.get('alsoKnownAs'):
alsoKnownAs = profileJson['alsoKnownAs']
joinedDate = None
if profileJson.get('published'):
if 'T' in profileJson['published']:
joinedDate = profileJson['published']
2020-11-12 23:38:58 +00:00
profileStr = \
_getProfileHeaderAfterSearch(baseDir,
nickname, defaultTimeline,
searchNickname,
searchDomainFull,
translate,
displayName, followsYou,
profileDescriptionShort,
avatarUrl, imageUrl,
movedTo, profileJson['id'],
alsoKnownAs, accessKeys,
joinedDate)
2020-11-12 22:41:41 +00:00
2020-12-16 11:19:16 +00:00
domainFull = getFullDomain(domain, port)
2020-11-20 12:14:22 +00:00
2020-11-20 12:20:34 +00:00
followIsPermitted = True
if not profileJson.get('followers'):
# no followers collection specified within actor
followIsPermitted = False
elif searchNickname == 'news' and searchDomainFull == domainFull:
2020-11-20 12:20:34 +00:00
# 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:
2021-09-21 18:57:12 +00:00
followStr = 'Follow'
if isGroup:
followStr = 'Join'
2020-11-20 12:14:22 +00:00
profileStr += \
2021-07-22 10:22:01 +00:00
'<div class="container">\n' + \
' <form method="POST" action="' + \
backUrl + '/followconfirm">\n' + \
' <center>\n' + \
2020-11-20 12:14:22 +00:00
' <input type="hidden" name="actor" value="' + \
2021-07-22 10:22:01 +00:00
personUrl + '">\n' + \
2021-04-23 12:04:42 +00:00
' <button type="submit" class="button" name="submitYes" ' + \
'accesskey="' + accessKeys['followButton'] + '">' + \
2021-09-21 18:57:12 +00:00
translate[followStr] + '</button>\n' + \
2021-07-28 14:02:07 +00:00
' <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'
2020-11-12 17:05:38 +00:00
2021-08-02 20:17:55 +00:00
userFeed = \
parseUserFeed(signingPrivateKeyPem,
session, outboxUrl, asHeader, projectVersion,
2021-08-02 20:17:55 +00:00
httpPrefix, domain, debug)
if userFeed:
i = 0
for item in userFeed:
2021-09-11 14:17:37 +00:00
isAnnouncedFeedItem = False
if isCreateInsideAnnounce(item):
2021-09-11 14:17:37 +00:00
isAnnouncedFeedItem = True
item = item['object']
2021-08-02 20:17:55 +00:00
if not item.get('actor'):
continue
2021-09-11 14:17:37 +00:00
if not isAnnouncedFeedItem and item['actor'] != personUrl:
2021-08-02 20:17:55 +00:00
continue
if not item.get('type'):
continue
2021-09-12 21:49:44 +00:00
if item['type'] == 'Create':
if not hasObjectDict(item):
continue
if item['type'] != 'Create' and item['type'] != 'Announce':
2021-08-02 20:17:55 +00:00
continue
2020-12-18 18:12:33 +00:00
2021-08-02 20:17:55 +00:00
profileStr += \
individualPostAsHtml(signingPrivateKeyPem,
True, recentPostsCache, maxRecentPosts,
2021-08-02 20:17:55 +00:00
translate, None, baseDir,
session, cachedWebfingers, personCache,
nickname, domain, port,
item, avatarUrl, False, False,
httpPrefix, projectVersion, 'inbox',
YTReplacementDomain,
2021-09-18 17:08:14 +00:00
twitterReplacementDomain,
2021-08-02 20:17:55 +00:00
showPublishedDateOnly,
peertubeInstances,
allowLocalNetworkAccess,
themeName, systemLanguage, maxLikeCount,
2021-08-02 20:17:55 +00:00
False, False, False, False, False)
i += 1
if i >= 8:
2021-08-02 20:17:55 +00:00
break
2020-11-09 22:44:03 +00:00
2021-01-11 19:46:21 +00:00
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
return htmlHeaderWithExternalStyle(cssFilename, instanceTitle) + \
profileStr + htmlFooter()
2020-11-09 22:44:03 +00:00
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,
2021-01-22 20:59:43 +00:00
theme: str, movedTo: str,
2021-01-24 18:09:21 +00:00
alsoKnownAs: [],
2021-04-23 12:04:42 +00:00
pinnedContent: str,
accessKeys: {},
2021-05-12 18:31:41 +00:00
joinedDate: str,
occupationName: str) -> str:
2020-11-12 22:33:00 +00:00
"""The header of the profile screen, containing background
image and avatar
"""
2021-07-22 10:22:01 +00:00
htmlStr = \
'\n\n <figure class="profileHeader">\n' + \
' <a href="/users/' + \
2020-11-12 22:33:00 +00:00
nickname + '/' + defaultTimeline + '" title="' + \
2021-07-22 10:22:01 +00:00
translate['Switch to timeline view'] + '">\n' + \
' <img class="profileBackground" ' + \
'alt="" ' + \
2021-07-22 10:22:01 +00:00
'src="/users/' + nickname + '/image_' + theme + '.png" /></a>\n' + \
' <figcaption>\n' + \
2020-11-12 23:17:37 +00:00
' <a href="/users/' + \
nickname + '/' + defaultTimeline + '" title="' + \
translate['Switch to timeline view'] + '">\n' + \
' <img loading="lazy" src="' + avatarUrl + '" ' + \
'alt="" class="title"></a>\n'
2021-05-12 18:31:41 +00:00
occupationStr = ''
if occupationName:
occupationStr += \
' <b>' + occupationName + '</b><br>\n'
htmlStr += ' <h1>' + displayName + '</h1>\n' + occupationStr
2020-11-12 22:33:00 +00:00
htmlStr += \
2020-11-12 23:00:36 +00:00
' <p><b>@' + nickname + '@' + domainFull + '</b><br>\n'
if joinedDate:
htmlStr += \
2021-05-08 18:00:59 +00:00
' <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'
2021-01-22 20:59:43 +00:00
elif alsoKnownAs:
otherAccountsHtml = \
2021-01-22 20:59:43 +00:00
' <p>' + translate['Other accounts'] + ': '
2021-08-14 11:13:39 +00:00
actor = localActorUrl(httpPrefix, nickname, domainFull)
ctr = 0
2021-01-22 20:59:43 +00:00
if isinstance(alsoKnownAs, list):
for altActor in alsoKnownAs:
if altActor == actor:
continue
2021-01-22 20:59:43 +00:00
if ctr > 0:
otherAccountsHtml += ' '
2021-01-22 20:59:43 +00:00
ctr += 1
altDomain, altPort = getDomainFromActor(altActor)
otherAccountsHtml += \
2021-01-22 20:59:43 +00:00
'<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
2020-11-12 22:33:00 +00:00
htmlStr += \
2020-11-12 23:00:36 +00:00
' <a href="/users/' + nickname + \
2020-11-12 22:33:00 +00:00
'/qrcode.png" alt="' + translate['QR Code'] + '" title="' + \
translate['QR Code'] + '">' + \
2021-02-01 18:42:39 +00:00
'<img class="qrcode" alt="' + translate['QR Code'] + \
2021-07-22 10:22:01 +00:00
'" src="/icons/qrcode.png" /></a></p>\n' + \
' <p>' + profileDescriptionShort + '</p>\n' + loginButton
2021-01-24 18:09:21 +00:00
if pinnedContent:
2021-01-28 14:21:51 +00:00
htmlStr += pinnedContent.replace('<p>', '<p>📎', 1)
2021-07-22 10:22:01 +00:00
htmlStr += \
' </figcaption>\n' + \
' </figure>\n\n'
2020-11-12 22:33:00 +00:00
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,
2021-04-23 12:04:42 +00:00
alsoKnownAs: [],
accessKeys: {},
joinedDate: str) -> str:
2020-11-12 23:38:58 +00:00
"""The header of a searched for handle, containing background
image and avatar
"""
if not imageUrl:
imageUrl = '/defaultprofilebackground'
2021-07-22 10:22:01 +00:00
htmlStr = \
'\n\n <figure class="profileHeader">\n' + \
' <a href="/users/' + \
nickname + '/' + defaultTimeline + '" title="' + \
2021-04-23 12:04:42 +00:00
translate['Switch to timeline view'] + '" ' + \
2021-07-22 10:22:01 +00:00
'accesskey="' + accessKeys['menuTimeline'] + '">\n' + \
' <img class="profileBackground" ' + \
'alt="" ' + \
2021-07-22 10:22:01 +00:00
'src="' + imageUrl + '" /></a>\n' + \
' <figcaption>\n'
2020-11-12 23:38:58 +00:00
if avatarUrl:
htmlStr += \
2021-07-22 10:22:01 +00:00
' <a href="/users/' + \
nickname + '/' + defaultTimeline + '" title="' + \
translate['Switch to timeline view'] + '">\n' + \
2020-11-12 23:38:58 +00:00
' <img loading="lazy" src="' + avatarUrl + '" ' + \
'alt="" class="title"></a>\n'
2021-09-13 13:47:00 +00:00
if not displayName:
displayName = searchNickname
2020-11-12 23:38:58 +00:00
htmlStr += \
2021-07-22 10:22:01 +00:00
' <h1>' + displayName + '</h1>\n' + \
2020-11-12 23:38:58 +00:00
' <p><b>@' + searchNickname + '@' + searchDomainFull + '</b><br>\n'
if joinedDate:
2021-05-08 18:00:59 +00:00
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'] + \
2021-08-20 13:51:33 +00:00
': <a href="' + movedTo + '">@' + newHandle + '</a></p>\n'
2021-01-22 20:48:52 +00:00
elif alsoKnownAs:
otherAccountshtml = \
2021-01-22 20:48:52 +00:00
' <p>' + translate['Other accounts'] + ': '
ctr = 0
2021-01-22 20:48:52 +00:00
if isinstance(alsoKnownAs, list):
for altActor in alsoKnownAs:
if altActor == actor:
continue
2021-01-22 20:48:52 +00:00
if ctr > 0:
otherAccountshtml += ' '
2021-01-22 20:48:52 +00:00
ctr += 1
altDomain, altPort = getDomainFromActor(altActor)
otherAccountshtml += \
2021-01-22 20:48:52 +00:00
'<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
2021-07-22 10:22:01 +00:00
htmlStr += \
' <p>' + profileDescriptionShort + '</p>\n' + \
' </figcaption>\n' + \
' </figure>\n\n'
2020-11-12 23:38:58 +00:00
return htmlStr
def htmlProfile(signingPrivateKeyPem: str,
rssIconAtTop: bool,
2020-11-09 22:44:03 +00:00
cssCache: {}, iconsAsButtons: bool,
defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, projectVersion: str,
baseDir: str, httpPrefix: str, authorized: bool,
profileJson: {}, selected: str,
session, cachedWebfingers: {}, personCache: {},
2020-11-09 22:44:03 +00:00
YTReplacementDomain: str,
2021-09-18 17:08:14 +00:00
twitterReplacementDomain: str,
2020-11-09 22:44:03 +00:00
showPublishedDateOnly: bool,
2020-12-20 17:26:38 +00:00
newswire: {}, theme: str, dormantMonths: int,
2020-12-23 23:59:49 +00:00
peertubeInstances: [],
allowLocalNetworkAccess: bool,
2021-02-05 19:14:27 +00:00
textModeBanner: str,
debug: bool, accessKeys: {}, city: str,
systemLanguage: str, maxLikeCount: int,
sharedItemsFederatedDomains: [],
2021-06-20 11:28:35 +00:00
extraJson: {} = None, pageNumber: int = None,
maxItemsPerPage: int = None) -> str:
2020-11-09 22:44:03 +00:00
"""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,
2021-09-18 17:08:14 +00:00
twitterReplacementDomain,
showPublishedDateOnly,
2020-12-20 17:26:38 +00:00
newswire, theme, extraJson,
2021-04-23 16:29:03 +00:00
allowLocalNetworkAccess, accessKeys,
systemLanguage, maxLikeCount,
sharedItemsFederatedDomains,
pageNumber, maxItemsPerPage)
2020-11-09 22:44:03 +00:00
domain, port = getDomainFromActor(profileJson['id'])
if not domain:
return ""
displayName = \
addEmojiToDisplayName(baseDir, httpPrefix,
nickname, domain,
profileJson['name'], True)
2021-06-13 08:48:59 +00:00
domainFull = getFullDomain(domain, port)
2020-11-09 22:44:03 +00:00
profileDescription = \
addEmojiToDisplayName(baseDir, httpPrefix,
nickname, domain,
profileJson['summary'], False)
postsButton = 'button'
followingButton = 'button'
followersButton = 'button'
rolesButton = 'button'
skillsButton = 'button'
sharesButton = 'button'
2021-08-09 19:37:18 +00:00
wantedButton = 'button'
2020-11-09 22:44:03 +00:00
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'
2021-08-09 19:37:18 +00:00
elif selected == 'wanted':
wantedButton = 'buttonselected'
2020-11-09 22:44:03 +00:00
loginButton = ''
followApprovalsSection = ''
followApprovals = False
editProfileStr = ''
logoutStr = ''
actor = profileJson['id']
usersPath = '/users/' + actor.split('/users/')[1]
donateSection = ''
donateUrl = getDonationUrl(profileJson)
2021-08-12 20:56:00 +00:00
websiteUrl = getWebsite(profileJson, translate)
2021-08-20 13:56:52 +00:00
blogAddress = getBlogAddress(profileJson)
2020-11-09 22:44:03 +00:00
PGPpubKey = getPGPpubKey(profileJson)
PGPfingerprint = getPGPfingerprint(profileJson)
emailAddress = getEmailAddress(profileJson)
xmppAddress = getXmppAddress(profileJson)
matrixAddress = getMatrixAddress(profileJson)
ssbAddress = getSSBAddress(profileJson)
toxAddress = getToxAddress(profileJson)
2020-12-24 16:48:03 +00:00
briarAddress = getBriarAddress(profileJson)
2020-11-29 12:50:41 +00:00
jamiAddress = getJamiAddress(profileJson)
2021-06-27 11:48:03 +00:00
cwtchAddress = getCwtchAddress(profileJson)
2021-08-12 21:08:55 +00:00
if donateUrl or websiteUrl or xmppAddress or matrixAddress or \
2020-12-24 16:48:03 +00:00
ssbAddress or toxAddress or briarAddress or \
2021-06-27 11:48:03 +00:00
jamiAddress or cwtchAddress or PGPpubKey or \
2020-11-09 22:44:03 +00:00
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'
2021-08-12 21:08:55 +00:00
if websiteUrl:
donateSection += \
'<p>' + translate['Website'] + ': <a href="' + \
websiteUrl + '">' + websiteUrl + '</a></p>\n'
2020-11-09 22:44:03 +00:00
if emailAddress:
donateSection += \
'<p>' + translate['Email'] + ': <a href="mailto:' + \
emailAddress + '">' + emailAddress + '</a></p>\n'
2021-08-20 13:56:52 +00:00
if blogAddress:
donateSection += \
'<p>Blog: <a href="' + \
blogAddress + '">' + blogAddress + '</a></p>\n'
2020-11-09 22:44:03 +00:00
if xmppAddress:
donateSection += \
'<p>' + translate['XMPP'] + ': <a href="xmpp:' + \
2021-06-22 12:42:52 +00:00
xmppAddress + '">' + xmppAddress + '</a></p>\n'
2020-11-09 22:44:03 +00:00
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'
2020-12-24 16:48:03 +00:00
if briarAddress:
2020-12-24 17:11:18 +00:00
if briarAddress.startswith('briar://'):
donateSection += \
'<p><label class="toxaddr">' + \
briarAddress + '</label></p>\n'
else:
donateSection += \
'<p>briar://<label class="toxaddr">' + \
briarAddress + '</label></p>\n'
2020-11-29 12:50:41 +00:00
if jamiAddress:
donateSection += \
'<p>Jami: <label class="toxaddr">' + \
jamiAddress + '</label></p>\n'
2021-06-27 11:48:03 +00:00
if cwtchAddress:
donateSection += \
'<p>Cwtch: <label class="toxaddr">' + \
cwtchAddress + '</label></p>\n'
2020-11-09 22:44:03 +00:00
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:
2020-11-09 22:44:03 +00:00
editProfileStr = \
'<a class="imageAnchor" href="' + usersPath + '/editprofile">' + \
2020-12-09 13:08:26 +00:00
'<img loading="lazy" src="/icons' + \
2020-11-09 22:44:03 +00:00
'/edit.png" title="' + translate['Edit'] + \
'" alt="| ' + translate['Edit'] + '" class="timelineicon"/></a>\n'
logoutStr = \
'<a class="imageAnchor" href="/logout">' + \
2020-12-09 13:08:26 +00:00
'<img loading="lazy" src="/icons' + \
2020-11-09 22:44:03 +00:00
'/logout.png" title="' + translate['Logout'] + \
'" alt="| ' + translate['Logout'] + \
'" class="timelineicon"/></a>\n'
# are there any follow requests?
followRequestsFilename = \
2021-07-13 21:59:53 +00:00
acctDir(baseDir, nickname, domain) + '/followrequests.txt'
2020-11-09 22:44:03 +00:00
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:
2021-08-14 11:13:39 +00:00
nick = followerHandle.split('@')[0]
dom = followerHandle.split('@')[1]
2020-11-09 22:44:03 +00:00
followerActor = \
2021-08-14 11:13:39 +00:00
localActorUrl(httpPrefix, nick, dom)
2020-11-09 22:44:03 +00:00
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']
2021-08-20 13:45:42 +00:00
if '"' in movedTo:
movedTo = movedTo.split('"')[1]
2021-01-22 20:59:43 +00:00
alsoKnownAs = None
if profileJson.get('alsoKnownAs'):
alsoKnownAs = profileJson['alsoKnownAs']
joinedDate = None
if profileJson.get('published'):
if 'T' in profileJson['published']:
joinedDate = profileJson['published']
2021-05-12 18:31:41 +00:00
occupationName = None
2021-05-13 11:35:36 +00:00
if profileJson.get('hasOccupation'):
2021-05-16 15:10:39 +00:00
occupationName = getOccupationName(profileJson)
avatarUrl = profileJson['icon']['url']
2021-06-13 08:48:59 +00:00
# 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/')
2021-01-24 18:09:21 +00:00
# get pinned post content
2021-07-13 21:59:53 +00:00
accountDir = acctDir(baseDir, nickname, domain)
2021-01-24 18:09:21 +00:00
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,
2021-01-24 18:09:21 +00:00
movedTo, alsoKnownAs,
pinnedContent, accessKeys,
2021-05-12 18:31:41 +00:00
joinedDate, occupationName)
2020-11-09 22:44:03 +00:00
2021-02-05 15:30:51 +00:00
# keyboard navigation
2021-02-05 17:05:53 +00:00
userPathStr = '/users/' + nickname
2021-02-05 17:44:50 +00:00
deft = defaultTimeline
isGroup = False
followersStr = translate['Followers']
if isGroupAccount(baseDir, nickname, domain):
isGroup = True
followersStr = translate['Members']
2021-02-06 10:35:47 +00:00
menuTimeline = \
2021-02-06 10:46:03 +00:00
htmlHideFromScreenReader('🏠') + ' ' + \
translate['Switch to timeline view']
2021-02-06 10:35:47 +00:00
menuEdit = \
2021-02-06 10:46:03 +00:00
htmlHideFromScreenReader('') + ' ' + translate['Edit']
if not isGroup:
menuFollowing = \
htmlHideFromScreenReader('👥') + ' ' + translate['Following']
2021-02-06 10:35:47 +00:00
menuFollowers = \
2021-09-21 17:20:45 +00:00
htmlHideFromScreenReader('👪') + ' ' + followersStr
if not isGroup:
menuRoles = \
htmlHideFromScreenReader('🤚') + ' ' + translate['Roles']
menuSkills = \
htmlHideFromScreenReader('🛠') + ' ' + translate['Skills']
2021-02-06 10:35:47 +00:00
menuLogout = \
2021-02-06 10:46:03 +00:00
htmlHideFromScreenReader('') + ' ' + translate['Logout']
2021-02-05 17:05:53 +00:00
navLinks = {
2021-02-06 10:35:47 +00:00
menuTimeline: userPathStr + '/' + deft,
menuEdit: userPathStr + '/editprofile',
menuFollowing: userPathStr + '/following#timeline',
menuFollowers: userPathStr + '/followers#timeline',
menuRoles: userPathStr + '/roles#timeline',
menuSkills: userPathStr + '/skills#timeline',
menuLogout: '/logout'
2021-02-05 17:55:49 +00:00
}
2021-04-22 14:12:59 +00:00
navAccessKeys = {}
for variableName, key in accessKeys.items():
if not locals().get(variableName):
continue
navAccessKeys[locals()[variableName]] = key
2021-04-22 11:51:19 +00:00
profileStr = htmlKeyboardNavigation(textModeBanner,
2021-04-22 11:57:35 +00:00
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>'
if not isGroup:
profileStr += \
' <a href="' + usersPath + '/following#buttonheader">' + \
'<button class="' + followingButton + '"><span>' + \
translate['Following'] + ' </span></button></a>'
profileStr += \
' <a href="' + usersPath + '/followers#buttonheader">' + \
'<button class="' + followersButton + \
2021-09-21 17:20:45 +00:00
'"><span>' + followersStr + ' </span></button></a>'
if not isGroup:
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>'
2020-11-09 22:44:03 +00:00
2021-02-05 17:55:49 +00:00
# start of #timeline
profileStr += '<div id="timeline">\n'
2020-11-09 22:44:03 +00:00
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':
2020-11-27 18:50:54 +00:00
profileStr += \
_htmlProfilePosts(recentPostsCache, maxRecentPosts,
translate,
baseDir, httpPrefix, authorized,
nickname, domain, port,
session, cachedWebfingers, personCache,
projectVersion,
YTReplacementDomain,
2021-09-18 17:08:14 +00:00
twitterReplacementDomain,
2020-12-23 23:59:49 +00:00
showPublishedDateOnly,
peertubeInstances,
2021-03-09 19:52:10 +00:00
allowLocalNetworkAccess,
theme, systemLanguage,
maxLikeCount,
signingPrivateKeyPem) + licenseStr
if not isGroup:
if selected == 'following':
profileStr += \
_htmlProfileFollowing(translate, baseDir, httpPrefix,
authorized, nickname,
domain, port, session,
cachedWebfingers, personCache, extraJson,
projectVersion, ["unfollow"], selected,
usersPath, pageNumber, maxItemsPerPage,
2021-09-21 17:47:25 +00:00
dormantMonths, debug,
signingPrivateKeyPem)
if selected == 'followers':
profileStr += \
_htmlProfileFollowing(translate, baseDir, httpPrefix,
authorized, nickname,
domain, port, session,
cachedWebfingers, personCache, extraJson,
projectVersion, ["block"],
selected, usersPath, pageNumber,
maxItemsPerPage, dormantMonths, debug,
signingPrivateKeyPem)
if not isGroup:
if selected == 'roles':
profileStr += \
_htmlProfileRoles(translate, nickname, domainFull,
extraJson)
2021-09-21 17:47:25 +00:00
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
2021-02-05 17:55:49 +00:00
# end of #timeline
profileStr += '</div>'
2020-11-12 17:05:38 +00:00
2021-01-11 19:46:21 +00:00
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
2020-11-12 17:05:38 +00:00
profileStr = \
htmlHeaderWithPersonMarkup(cssFilename, instanceTitle,
profileJson, city) + \
profileStr + htmlFooter()
2020-11-09 22:44:03 +00:00
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,
2021-09-18 17:08:14 +00:00
twitterReplacementDomain: str,
2020-12-23 23:59:49 +00:00
showPublishedDateOnly: bool,
peertubeInstances: [],
2021-03-09 19:52:10 +00:00
allowLocalNetworkAccess: bool,
themeName: str, systemLanguage: str,
maxLikeCount: int,
signingPrivateKeyPem: str) -> str:
2020-11-09 22:44:03 +00:00
"""Shows posts on the profile screen
These should only be public posts
"""
separatorStr = htmlPostSeparator(baseDir, None)
profileStr = ''
maxItems = 4
ctr = 0
currPage = 1
2020-11-27 12:29:20 +00:00
boxName = 'outbox'
2020-11-09 22:44:03 +00:00
while ctr < maxItems and currPage < 4:
outboxFeedPathStr = \
'/users/' + nickname + '/' + boxName + '?page=' + \
str(currPage)
2020-11-09 22:44:03 +00:00
outboxFeed = \
personBoxJson({}, session, baseDir, domain,
port,
outboxFeedPathStr,
2020-11-09 22:44:03 +00:00
httpPrefix,
2020-11-27 12:29:20 +00:00
10, boxName,
2020-11-09 22:44:03 +00:00
authorized, 0, False, 0)
if not outboxFeed:
2020-11-27 16:14:54 +00:00
break
2020-11-09 22:44:03 +00:00
if len(outboxFeed['orderedItems']) == 0:
break
for item in outboxFeed['orderedItems']:
if item['type'] == 'Create':
postStr = \
individualPostAsHtml(signingPrivateKeyPem,
True, recentPostsCache,
2020-11-09 22:44:03 +00:00
maxRecentPosts,
2020-12-09 13:31:54 +00:00
translate, None,
baseDir, session, cachedWebfingers,
2020-11-09 22:44:03 +00:00
personCache,
nickname, domain, port, item,
None, True, False,
httpPrefix, projectVersion, 'inbox',
YTReplacementDomain,
2021-09-18 17:08:14 +00:00
twitterReplacementDomain,
2020-11-09 22:44:03 +00:00
showPublishedDateOnly,
2020-12-23 23:59:49 +00:00
peertubeInstances,
allowLocalNetworkAccess,
themeName, systemLanguage,
maxLikeCount,
2020-11-09 22:44:03 +00:00
False, False, False, True, False)
if postStr:
2020-11-18 21:18:51 +00:00
profileStr += postStr + separatorStr
2020-11-09 22:44:03 +00:00
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:
2020-11-09 22:44:03 +00:00
"""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 + \
2020-11-24 14:49:07 +00:00
'?page=' + str(pageNumber - 1) + '#buttonheader' + \
2020-11-09 22:44:03 +00:00
'"><img loading="lazy" class="pageicon" src="/' + \
2020-12-09 13:08:26 +00:00
'icons/pageup.png" title="' + \
2020-11-09 22:44:03 +00:00
translate['Page up'] + '" alt="' + \
translate['Page up'] + '"></a>\n' + \
' </center>\n'
for followingActor in followingJson['orderedItems']:
2020-12-13 12:48:04 +00:00
# is this a dormant followed account?
dormant = False
2020-12-13 12:45:29 +00:00
if authorized and feedName == 'following':
dormant = \
isDormant(baseDir, nickname, domain, followingActor,
dormantMonths)
2020-12-13 12:48:04 +00:00
2020-11-09 22:44:03 +00:00
profileStr += \
_individualFollowAsHtml(signingPrivateKeyPem,
translate, baseDir, session,
cachedWebfingers, personCache,
domain, followingActor,
authorized, nickname,
httpPrefix, projectVersion, dormant,
2021-03-14 19:22:58 +00:00
debug, buttons)
2020-12-13 12:48:04 +00:00
2020-11-09 22:44:03 +00:00
if authorized and maxItemsPerPage and pageNumber:
if len(followingJson['orderedItems']) >= maxItemsPerPage:
# page down arrow
profileStr += \
' <center>\n' + \
' <a href="' + actor + '/' + feedName + \
2020-11-24 14:49:07 +00:00
'?page=' + str(pageNumber + 1) + '#buttonheader' + \
2020-11-09 22:44:03 +00:00
'"><img loading="lazy" class="pageicon" src="/' + \
2020-12-09 13:08:26 +00:00
'icons/pagedown.png" title="' + \
2020-11-09 22:44:03 +00:00
translate['Page down'] + '" alt="' + \
translate['Page down'] + '"></a>\n' + \
' </center>\n'
return profileStr
def _htmlProfileRoles(translate: {}, nickname: str, domain: str,
rolesList: []) -> str:
2020-11-09 22:44:03 +00:00
"""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'
2020-11-09 22:44:03 +00:00
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:
2020-11-09 22:44:03 +00:00
"""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: {},
2021-08-09 20:05:14 +00:00
nickname: str, domain: str, sharesJson: {},
sharesFileType: str) -> str:
2020-11-09 22:44:03 +00:00
"""Shows shares on the profile screen
"""
profileStr = ''
for item in sharesJson['orderedItems']:
profileStr += htmlIndividualShare(domain, item['shareId'],
2021-08-09 20:05:14 +00:00
actor, item, translate, False, False,
sharesFileType)
2020-11-09 22:44:03 +00:00
if len(profileStr) > 0:
profileStr = '<div class="share-title">' + profileStr + '</div>\n'
return profileStr
2021-06-27 17:59:46 +00:00
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'
2021-07-22 12:36:31 +00:00
grayscale = _grayscaleEnabled(baseDir)
2021-06-27 17:59:46 +00:00
themesDropdown += \
2021-07-22 16:58:59 +00:00
editCheckBox(translate['Grayscale'], 'grayscale', grayscale)
2021-06-27 17:59:46 +00:00
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 += \
2021-07-22 16:58:59 +00:00
editCheckBox(translate['Remove the custom font'],
'removeCustomFont', False)
2021-06-27 17:59:46 +00:00
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'
2021-07-22 16:58:59 +00:00
graphicsStr = beginEditSection(translate['Graphic Design'])
2021-08-13 17:31:07 +00:00
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" ' + \
2021-08-13 17:35:31 +00:00
'name="submitExportTheme">➤</button><br>\n'
2021-08-13 17:34:23 +00:00
graphicsStr += \
editCheckBox(translate['Low Bandwidth'], 'lowBandwidth',
bool(lowBandwidth))
2021-07-22 16:58:59 +00:00
graphicsStr += endEditSection()
return graphicsStr
2021-09-18 17:08:14 +00:00
def _htmlEditProfileTwitter(baseDir: str, translate: {},
removeTwitter: str) -> str:
"""Edit twitter settings within profile
"""
# Twitter section
twitterStr = beginEditSection(translate['Twitter'])
twitterStr += \
editCheckBox(translate['Remove Twitter posts'],
'removeTwitter', removeTwitter)
twitterReplacementDomain = getConfigParam(baseDir, "twitterdomain")
if not twitterReplacementDomain:
twitterReplacementDomain = ''
twitterStr += \
editTextField(translate['Twitter Replacement Domain'],
'twitterdomain', twitterReplacementDomain)
twitterStr += endEditSection()
return twitterStr
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')
2021-07-22 16:58:59 +00:00
instanceStr = beginEditSection(translate['Instance Settings'])
2021-07-22 12:36:31 +00:00
instanceStr += \
2021-07-22 16:58:59 +00:00
editTextField(translate['Instance Title'],
'instanceTitle', instanceTitle)
2021-07-22 12:36:31 +00:00
instanceStr += '<br>\n'
instanceStr += \
2021-07-22 16:58:59 +00:00
editTextField(translate['Instance Short Description'],
'instanceDescriptionShort', instanceDescriptionShort)
2021-07-22 12:36:31 +00:00
instanceStr += '<br>\n'
instanceStr += \
2021-07-22 16:58:59 +00:00
editTextArea(translate['Instance Description'],
'instanceDescription', instanceDescription, 200,
'', True)
instanceStr += \
2021-07-22 16:58:59 +00:00
editTextField(translate['Custom post submit button text'],
'customSubmitText', customSubmitText)
2021-07-22 12:36:31 +00:00
instanceStr += '<br>\n'
instanceStr += \
' <label class="labels">' + \
2021-07-22 10:22:01 +00:00
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 += \
2021-07-22 16:58:59 +00:00
editCheckBox(nodeInfoStr, 'showNodeInfoAccounts', True)
else:
instanceStr += \
2021-07-22 16:58:59 +00:00
editCheckBox(nodeInfoStr, 'showNodeInfoAccounts', False)
nodeInfoStr = \
translate['Show version number within instance metadata']
if getConfigParam(baseDir, "showNodeInfoVersion"):
instanceStr += \
2021-07-22 16:58:59 +00:00
editCheckBox(nodeInfoStr, 'showNodeInfoVersion', True)
else:
instanceStr += \
2021-07-22 16:58:59 +00:00
editCheckBox(nodeInfoStr, 'showNodeInfoVersion', False)
if getConfigParam(baseDir, "verifyAllSignatures"):
instanceStr += \
2021-07-22 16:58:59 +00:00
editCheckBox(translate['Verify all signatures'],
'verifyallsignatures', True)
else:
instanceStr += \
2021-07-22 16:58:59 +00:00
editCheckBox(translate['Verify all signatures'],
'verifyallsignatures', False)
instanceStr += translate['Enabling broch mode'] + '<br>\n'
if getConfigParam(baseDir, "brochMode"):
instanceStr += \
2021-07-22 16:58:59 +00:00
editCheckBox(translate['Broch mode'], 'brochMode', True)
else:
instanceStr += \
2021-07-22 16:58:59 +00:00
editCheckBox(translate['Broch mode'], 'brochMode', False)
# Instance type
instanceStr += \
' <br><label class="labels">' + \
2021-07-22 12:36:31 +00:00
translate['Type of instance'] + '</label><br>\n'
instanceStr += \
2021-07-22 16:58:59 +00:00
editCheckBox(translate['This is a media instance'],
'mediaInstance', mediaInstanceStr)
2021-07-22 12:36:31 +00:00
instanceStr += \
2021-07-22 16:58:59 +00:00
editCheckBox(translate['This is a blogging instance'],
'blogsInstance', blogsInstanceStr)
2021-07-22 12:36:31 +00:00
instanceStr += \
2021-07-22 16:58:59 +00:00
editCheckBox(translate['This is a news instance'],
'newsInstance', newsInstanceStr)
2021-07-22 12:36:31 +00:00
2021-07-22 16:58:59 +00:00
instanceStr += endEditSection()
# Role assignments section
moderators = ''
moderatorsFile = baseDir + '/accounts/moderators.txt'
if os.path.isfile(moderatorsFile):
2021-07-13 14:40:49 +00:00
with open(moderatorsFile, 'r') as f:
moderators = f.read()
# site moderators
2021-07-22 10:22:01 +00:00
roleAssignStr = \
2021-07-22 16:58:59 +00:00
beginEditSection(translate['Role Assignment']) + \
2021-07-22 10:22:01 +00:00
' <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):
2021-07-13 14:40:49 +00:00
with open(editorsFile, 'r') as f:
editors = f.read()
roleAssignStr += \
2021-07-22 10:22:01 +00:00
' <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):
2021-07-13 14:40:49 +00:00
with open(counselorsFile, 'r') as f:
counselors = f.read()
roleAssignStr += \
2021-07-22 18:35:45 +00:00
editTextArea(translate['Counselors'], 'counselors', counselors,
200, '', False)
# artists
artists = ''
artistsFile = baseDir + '/accounts/artists.txt'
if os.path.isfile(artistsFile):
2021-07-13 14:40:49 +00:00
with open(artistsFile, 'r') as f:
artists = f.read()
roleAssignStr += \
2021-07-22 18:35:45 +00:00
editTextArea(translate['Artists'], 'artists', artists,
200, '', False)
roleAssignStr += endEditSection()
# Video section
2021-07-22 18:35:45 +00:00
peertubeStr = beginEditSection(translate['Video Settings'])
peertubeInstancesStr = ''
for url in peertubeInstances:
peertubeInstancesStr += url + '\n'
peertubeStr += \
2021-07-22 18:35:45 +00:00
editTextArea(translate['Peertube Instances'], 'ptInstances',
peertubeInstancesStr, 200, '', False)
peertubeStr += \
2021-07-22 12:36:31 +00:00
' <br>\n'
YTReplacementDomain = getConfigParam(baseDir, "youtubedomain")
if not YTReplacementDomain:
YTReplacementDomain = ''
peertubeStr += \
2021-07-22 16:58:59 +00:00
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
"""
2021-07-22 16:58:59 +00:00
editProfileForm = beginEditSection(translate['Danger Zone'])
2021-07-22 12:36:31 +00:00
editProfileForm += \
2021-07-22 10:22:01 +00:00
' <b><label class="labels">' + \
2021-07-22 12:36:31 +00:00
translate['Danger Zone'] + '</label></b><br>\n'
editProfileForm += \
2021-07-22 16:58:59 +00:00
editCheckBox(translate['Deactivate this account'],
'deactivateThisAccount', False)
2021-07-22 12:36:31 +00:00
2021-07-22 16:58:59 +00:00
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) + \
2021-07-22 10:22:01 +00:00
'" 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) + \
2021-07-22 10:22:01 +00:00
'" value="" style="width:40%">' + \
'<input type="range" min="1" max="100" ' + \
'class="slider" name="skillValue' + \
2021-07-22 16:58:59 +00:00
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.'
2021-07-22 10:22:01 +00:00
editProfileForm = \
2021-07-22 16:58:59 +00:00
beginEditSection(translate['Skills']) + \
2021-07-22 10:22:01 +00:00
' <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 = \
2021-07-13 21:59:53 +00:00
acctDir(baseDir, nickname, domain) + '/gitprojects.txt'
if os.path.isfile(gitProjectsFilename):
with open(gitProjectsFilename, 'r') as gitProjectsFile:
gitProjectsStr = gitProjectsFile.read()
2021-07-22 18:35:45 +00:00
editProfileForm = beginEditSection(translate['Git Projects'])
2021-07-22 19:00:37 +00:00
idx = 'List of project names that you wish to receive git patches for'
editProfileForm += \
2021-07-22 18:35:45 +00:00
editTextArea(translate[idx], 'gitProjects', gitProjectsStr,
100, '', False)
2021-07-22 19:00:37 +00:00
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: {},
replyIntervalHours: int) -> str:
"""Filtering and blocking section of edit profile screen
"""
filterStr = ''
filterFilename = \
2021-07-13 21:59:53 +00:00
acctDir(baseDir, nickname, domain) + '/filters.txt'
if os.path.isfile(filterFilename):
with open(filterFilename, 'r') as filterfile:
filterStr = filterfile.read()
switchStr = ''
switchFilename = \
2021-07-13 21:59:53 +00:00
acctDir(baseDir, nickname, domain) + '/replacewords.txt'
if os.path.isfile(switchFilename):
with open(switchFilename, 'r') as switchfile:
switchStr = switchfile.read()
autoTags = ''
autoTagsFilename = \
2021-07-13 21:59:53 +00:00
acctDir(baseDir, nickname, domain) + '/autotags.txt'
if os.path.isfile(autoTagsFilename):
with open(autoTagsFilename, 'r') as autoTagsFile:
autoTags = autoTagsFile.read()
autoCW = ''
autoCWFilename = \
2021-07-13 21:59:53 +00:00
acctDir(baseDir, nickname, domain) + '/autocw.txt'
if os.path.isfile(autoCWFilename):
with open(autoCWFilename, 'r') as autoCWFile:
autoCW = autoCWFile.read()
blockedStr = ''
blockedFilename = \
2021-07-13 21:59:53 +00:00
acctDir(baseDir, nickname, domain) + '/blocking.txt'
if os.path.isfile(blockedFilename):
with open(blockedFilename, 'r') as blockedfile:
blockedStr = blockedfile.read()
dmAllowedInstancesStr = ''
dmAllowedInstancesFilename = \
2021-07-13 21:59:53 +00:00
acctDir(baseDir, nickname, domain) + '/dmAllowedInstances.txt'
if os.path.isfile(dmAllowedInstancesFilename):
with open(dmAllowedInstancesFilename, 'r') as dmAllowedInstancesFile:
dmAllowedInstancesStr = dmAllowedInstancesFile.read()
allowedInstancesStr = ''
allowedInstancesFilename = \
2021-07-13 21:59:53 +00:00
acctDir(baseDir, nickname, domain) + '/allowedinstances.txt'
if os.path.isfile(allowedInstancesFilename):
with open(allowedInstancesFilename, 'r') as allowedInstancesFile:
allowedInstancesStr = allowedInstancesFile.read()
2021-07-22 16:58:59 +00:00
editProfileForm = beginEditSection(translate['Filtering and Blocking'])
idx = 'Hours after posting during which replies are allowed'
editProfileForm += \
' <label class="labels">' + \
translate[idx] + \
':</label> <input type="number" name="replyhours" ' + \
'min="0" max="999999999999" step="1" ' + \
'value="' + str(replyIntervalHours) + '"><br>\n'
editProfileForm += \
'<label class="labels">' + \
translate['City for spoofed GPS image metadata'] + \
'</label><br>\n'
city = ''
2021-07-13 21:59:53 +00:00
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 = []
2021-07-13 14:40:49 +00:00
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">' + \
2021-07-22 10:22:01 +00:00
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">' + \
2021-07-22 10:22:01 +00:00
filterStr + '</textarea>\n' + \
' <br><b><label class="labels">' + \
2021-07-22 10:22:01 +00:00
translate['Word Replacements'] + '</label></b>\n' + \
' <br><label class="labels">A -> B</label>\n' + \
' <textarea id="message" name="switchWords" ' + \
'style="height:200px" spellcheck="false">' + \
2021-07-22 10:22:01 +00:00
switchStr + '</textarea>\n' + \
' <br><b><label class="labels">' + \
2021-07-22 10:22:01 +00:00
translate['Autogenerated Hashtags'] + '</label></b>\n' + \
' <br><label class="labels">A -> #B</label>\n' + \
' <textarea id="message" name="autoTags" ' + \
'style="height:200px" spellcheck="false">' + \
2021-07-22 10:22:01 +00:00
autoTags + '</textarea>\n' + \
' <br><b><label class="labels">' + \
2021-07-22 10:22:01 +00:00
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 += \
2021-07-22 18:35:45 +00:00
editTextArea(translate['Blocked accounts'], 'blocked', blockedStr,
200, '', False)
idx = 'Direct messages are always allowed from these instances.'
editProfileForm += \
2021-07-22 18:35:45 +00:00
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 += \
2021-07-22 10:22:01 +00:00
' <br><b><label class="labels">' + \
translate['Federation list'] + '</label></b>\n' + \
' <br><label class="labels">' + \
2021-07-22 10:22:01 +00:00
translate[idx] + '</label>\n' + \
' <textarea id="message" name="allowedInstances" ' + \
'style="height:200px" spellcheck="false">' + \
allowedInstancesStr + '</textarea>\n'
userAgentsBlockedStr = ''
2021-07-02 21:18:54 +00:00
for ua in userAgentsBlocked:
if userAgentsBlockedStr:
userAgentsBlockedStr += '\n'
userAgentsBlockedStr += ua
editProfileForm += \
2021-07-22 18:35:45 +00:00
editTextArea(translate['Blocked User Agents'],
'userAgentsBlockedStr', userAgentsBlockedStr,
200, '', False)
2021-07-22 16:58:59 +00:00
editProfileForm += endEditSection()
return editProfileForm
def _htmlEditProfileChangePassword(translate: {}) -> str:
"""Change password section of edit profile screen
"""
2021-07-06 10:18:56 +00:00
editProfileForm = \
2021-07-22 16:58:59 +00:00
beginEditSection(translate['Change Password']) + \
'<label class="labels">' + translate['Change Password'] + \
2021-07-06 10:18:56 +00:00
'</label><br>\n' + \
2021-07-20 08:44:32 +00:00
' <input type="password" name="password" ' + \
2021-07-06 10:18:56 +00:00
'value=""><br>\n' + \
'<label class="labels">' + translate['Confirm Password'] + \
2021-07-06 10:18:56 +00:00
'</label><br>\n' + \
2021-07-20 08:44:32 +00:00
' <input type="password" name="passwordconfirm" value="">\n' + \
2021-07-22 16:58:59 +00:00
endEditSection()
return editProfileForm
2021-07-19 19:40:04 +00:00
def _htmlEditProfileLibreTranslate(translate: {},
libretranslateUrl: str,
libretranslateApiKey: str) -> str:
"""Change automatic translation settings
"""
2021-07-22 16:58:59 +00:00
editProfileForm = beginEditSection('LibreTranslate')
2021-07-19 19:40:04 +00:00
2021-07-22 13:26:43 +00:00
editProfileForm += \
2021-07-22 16:58:59 +00:00
editTextField('URL', 'libretranslateUrl', libretranslateUrl,
'http://0.0.0.0:5000')
2021-07-22 13:26:43 +00:00
editProfileForm += \
2021-07-22 16:58:59 +00:00
editTextField('API Key', 'libretranslateApiKey', libretranslateApiKey)
2021-07-22 13:26:43 +00:00
2021-07-22 16:58:59 +00:00
editProfileForm += endEditSection()
2021-07-19 19:40:04 +00:00
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.'
2021-07-06 10:18:56 +00:00
editProfileForm = \
2021-07-22 16:58:59 +00:00
beginEditSection(translate['Background Images']) + \
' <label class="labels">' + translate[idx] + '</label><br><br>\n'
if not newsInstance:
imageFormats = getImageFormats()
editProfileForm += \
' <label class="labels">' + \
2021-07-06 10:18:56 +00:00
translate['Background image'] + '</label>\n' + \
' <input type="file" id="image" name="image"' + \
2021-07-22 10:22:01 +00:00
' accept="' + imageFormats + '">\n' + \
2021-07-06 10:18:56 +00:00
' <br><label class="labels">' + \
translate['Timeline banner image'] + '</label>\n' + \
' <input type="file" id="banner" name="banner"' + \
2021-07-22 10:22:01 +00:00
' accept="' + imageFormats + '">\n' + \
2021-07-06 10:18:56 +00:00
' <br><label class="labels">' + \
translate['Search banner image'] + '</label>\n' + \
' <input type="file" id="search_banner" ' + \
'name="search_banner"' + \
2021-07-22 10:22:01 +00:00
' accept="' + imageFormats + '">\n' + \
2021-07-06 10:18:56 +00:00
' <br><label class="labels">' + \
translate['Left column image'] + '</label>\n' + \
' <input type="file" id="left_col_image" ' + \
'name="left_col_image"' + \
2021-07-22 10:22:01 +00:00
' accept="' + imageFormats + '">\n' + \
2021-07-06 10:18:56 +00:00
' <br><label class="labels">' + \
translate['Right column image'] + '</label>\n' + \
' <input type="file" id="right_col_image" ' + \
'name="right_col_image"' + \
' accept="' + imageFormats + '">\n'
2021-07-22 16:58:59 +00:00
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
"""
2021-07-22 16:58:59 +00:00
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 += \
2021-07-22 16:58:59 +00:00
editTextArea(translate['PGP'], 'pgp', PGPpubKey, 600,
'-----BEGIN PGP PUBLIC KEY BLOCK-----', False)
editProfileForm += \
2021-07-06 10:18:56 +00:00
'<a href="/users/' + nickname + \
'/followingaccounts"><label class="labels">' + \
2021-07-22 10:22:01 +00:00
translate['Following'] + '</label></a><br>\n'
2021-07-22 16:58:59 +00:00
editProfileForm += endEditSection()
return editProfileForm
2021-08-12 18:18:50 +00:00
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 += \
2021-07-22 16:58:59 +00:00
editCheckBox(translate['Approve follower requests'],
'approveFollowers', manuallyApprovesFollowers)
editProfileForm += \
2021-07-22 16:58:59 +00:00
editCheckBox(translate['This is a bot account'],
'isBot', isBot)
2021-08-12 18:18:50 +00:00
if isAdmin:
editProfileForm += \
editCheckBox(translate['This is a group account'],
'isGroup', isGroup)
editProfileForm += \
2021-07-22 16:58:59 +00:00
editCheckBox(translate['Only people I follow can send me DMs'],
'followDMs', followDMs)
editProfileForm += \
2021-07-22 16:58:59 +00:00
editCheckBox(translate['Remove Twitter posts'],
'removeTwitter', removeTwitter)
editProfileForm += \
2021-07-22 16:58:59 +00:00
editCheckBox(translate['Notify when posts are liked'],
'notifyLikes', notifyLikes)
editProfileForm += \
2021-07-22 16:58:59 +00:00
editCheckBox(translate["Don't show the Like button"],
'hideLikeButton', hideLikeButton)
editProfileForm += ' </div>\n'
return editProfileForm
2021-08-08 11:16:18 +00:00
def _getSupportedLanguagesSorted(baseDir: str) -> str:
"""Returns a list of supported languages
"""
2021-08-08 11:16:18 +00:00
langList = getSupportedLanguages(baseDir)
2021-07-18 18:57:08 +00:00
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,
2021-08-12 20:40:23 +00:00
movedTo: str, donateUrl: str, websiteUrl: str,
blogAddress: str, actorJson: {},
translate: {}) -> str:
"""main info on edit profile screen
"""
imageFormats = getImageFormats()
editProfileForm = ' <div class="container">\n'
2021-07-06 10:18:56 +00:00
editProfileForm += \
2021-07-22 16:58:59 +00:00
editTextField(translate['Nickname'], 'displayNickname',
displayNickname)
2021-07-06 10:18:56 +00:00
editProfileForm += \
2021-07-22 18:35:45 +00:00
editTextArea(translate['Your bio'], 'bio', bioStr, 200, '', True)
2021-07-06 10:18:56 +00:00
editProfileForm += \
' <label class="labels">' + translate['Avatar image'] + \
2021-07-06 10:18:56 +00:00
'</label>\n' + \
' <input type="file" id="avatar" name="avatar"' + \
' accept="' + imageFormats + '">\n'
occupationName = ''
if actorJson.get('hasOccupation'):
occupationName = getOccupationName(actorJson)
editProfileForm += \
2021-07-22 16:58:59 +00:00
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 += \
2021-07-22 16:58:59 +00:00
editTextField(translate['Other accounts'], 'alsoKnownAs',
alsoKnownAsStr, 'https://...')
editProfileForm += \
2021-07-22 16:58:59 +00:00
editTextField(translate['Moved to new account address'], 'movedTo',
movedTo, 'https://...')
2021-07-06 10:18:56 +00:00
editProfileForm += \
2021-07-22 16:58:59 +00:00
editTextField(translate['Donations link'], 'donateUrl',
donateUrl, 'https://...')
2021-07-06 10:18:56 +00:00
2021-08-12 20:40:23 +00:00
editProfileForm += \
2021-08-12 20:56:00 +00:00
editTextField(translate['Website'], 'websiteUrl',
websiteUrl, 'https://...')
2021-08-12 20:40:23 +00:00
editProfileForm += \
2021-07-22 16:58:59 +00:00
editTextField('Blog', 'blogAddress', blogAddress, 'https://...')
2021-08-08 11:16:18 +00:00
languagesListStr = _getSupportedLanguagesSorted(baseDir)
showLanguages = getActorLanguages(actorJson)
editProfileForm += \
2021-07-22 16:58:59 +00:00
editTextField(translate['Languages'], 'showLanguages',
showLanguages, languagesListStr)
2021-07-22 13:26:43 +00:00
2021-07-18 18:48:34 +00:00
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 = \
2021-07-06 10:18:56 +00:00
'<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 += \
2021-07-22 16:58:59 +00:00
editCheckBox(translate['Remove scheduled posts'],
'removeScheduledPosts', False)
editProfileForm += ' </div>\n'
return editProfileForm
2020-11-09 22:44:03 +00:00
def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str,
domain: str, port: int, httpPrefix: str,
defaultTimeline: str, theme: str,
2021-02-05 19:14:27 +00:00
peertubeInstances: [],
2021-05-30 14:27:26 +00:00
textModeBanner: str, city: str,
userAgentsBlocked: str,
accessKeys: {},
defaultReplyIntervalHours: int) -> str:
2020-11-09 22:44:03 +00:00
"""Shows the edit profile screen
"""
path = path.replace('/inbox', '').replace('/outbox', '')
2021-08-09 19:37:18 +00:00
path = path.replace('/shares', '').replace('/wanted', '')
2020-11-09 22:44:03 +00:00
nickname = getNicknameFromActor(path)
if not nickname:
return ''
2020-12-16 11:19:16 +00:00
domainFull = getFullDomain(domain, port)
2020-11-09 22:44:03 +00:00
2021-07-13 21:59:53 +00:00
actorFilename = acctDir(baseDir, nickname, domain) + '.json'
2020-11-09 22:44:03 +00:00
if not os.path.isfile(actorFilename):
return ''
# filename of the banner shown at the top
2020-12-20 18:16:53 +00:00
bannerFile, bannerFilename = \
getBannerFile(baseDir, nickname, domain, theme)
2020-11-09 22:44:03 +00:00
displayNickname = nickname
2021-06-27 16:22:55 +00:00
isBot = isGroup = followDMs = removeTwitter = ''
notifyLikes = hideLikeButton = mediaInstanceStr = ''
2021-09-18 17:08:14 +00:00
blogsInstanceStr = newsInstanceStr = movedTo = twitterStr = ''
2021-08-12 21:08:55 +00:00
bioStr = donateUrl = websiteUrl = emailAddress = PGPpubKey = ''
2021-06-27 16:22:55 +00:00
PGPfingerprint = xmppAddress = matrixAddress = ''
ssbAddress = blogAddress = toxAddress = jamiAddress = ''
cwtchAddress = briarAddress = manuallyApprovesFollowers = ''
2020-11-09 22:44:03 +00:00
actorJson = loadJson(actorFilename)
if actorJson:
2021-01-12 12:35:30 +00:00
if actorJson.get('movedTo'):
movedTo = actorJson['movedTo']
2020-11-09 22:44:03 +00:00
donateUrl = getDonationUrl(actorJson)
2021-08-12 20:56:00 +00:00
websiteUrl = getWebsite(actorJson, translate)
2020-11-09 22:44:03 +00:00
xmppAddress = getXmppAddress(actorJson)
matrixAddress = getMatrixAddress(actorJson)
ssbAddress = getSSBAddress(actorJson)
blogAddress = getBlogAddress(actorJson)
toxAddress = getToxAddress(actorJson)
2020-12-24 16:48:03 +00:00
briarAddress = getBriarAddress(actorJson)
2020-11-29 12:50:41 +00:00
jamiAddress = getJamiAddress(actorJson)
2021-06-27 11:48:03 +00:00
cwtchAddress = getCwtchAddress(actorJson)
2020-11-09 22:44:03 +00:00
emailAddress = getEmailAddress(actorJson)
PGPpubKey = getPGPpubKey(actorJson)
PGPfingerprint = getPGPfingerprint(actorJson)
if actorJson.get('name'):
2020-12-19 12:55:40 +00:00
if not isFiltered(baseDir, nickname, domain, actorJson['name']):
displayNickname = actorJson['name']
2020-11-09 22:44:03 +00:00
if actorJson.get('summary'):
bioStr = \
actorJson['summary'].replace('<p>', '').replace('</p>', '')
2020-12-19 12:51:13 +00:00
if isFiltered(baseDir, nickname, domain, bioStr):
bioStr = ''
2020-11-09 22:44:03 +00:00
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 = ''
2021-07-13 21:59:53 +00:00
accountDir = acctDir(baseDir, nickname, domain)
if os.path.isfile(accountDir + '/.followDMs'):
2020-11-09 22:44:03 +00:00
followDMs = 'checked'
2021-07-13 21:59:53 +00:00
if os.path.isfile(accountDir + '/.removeTwitter'):
2020-11-09 22:44:03 +00:00
removeTwitter = 'checked'
2021-07-13 21:59:53 +00:00
if os.path.isfile(accountDir + '/.notifyLikes'):
2020-11-09 22:44:03 +00:00
notifyLikes = 'checked'
2021-07-13 21:59:53 +00:00
if os.path.isfile(accountDir + '/.hideLikeButton'):
2020-11-09 22:44:03 +00:00
hideLikeButton = 'checked'
mediaInstance = getConfigParam(baseDir, "mediaInstance")
if mediaInstance:
if mediaInstance is True:
mediaInstanceStr = 'checked'
2021-06-27 16:24:12 +00:00
blogsInstanceStr = newsInstanceStr = ''
2020-11-09 22:44:03 +00:00
newsInstance = getConfigParam(baseDir, "newsInstance")
if newsInstance:
if newsInstance is True:
newsInstanceStr = 'checked'
2021-06-27 16:24:12 +00:00
blogsInstanceStr = mediaInstanceStr = ''
2020-11-09 22:44:03 +00:00
blogsInstance = getConfigParam(baseDir, "blogsInstance")
if blogsInstance:
if blogsInstance is True:
blogsInstanceStr = 'checked'
2021-06-27 16:24:12 +00:00
mediaInstanceStr = newsInstanceStr = ''
2020-11-09 22:44:03 +00:00
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
instanceStr = ''
2021-03-06 13:40:47 +00:00
roleAssignStr = ''
2020-12-24 11:56:17 +00:00
peertubeStr = ''
libretranslateStr = ''
2021-07-20 20:21:25 +00:00
graphicsStr = ''
sharesFederationStr = ''
2020-11-09 22:44:03 +00:00
adminNickname = getConfigParam(baseDir, 'admin')
if isArtist(baseDir, nickname) or \
path.startswith('/users/' + str(adminNickname) + '/'):
graphicsStr = _htmlEditProfileGraphicDesign(baseDir, translate)
2021-08-12 18:18:50 +00:00
isAdmin = False
2020-11-09 22:44:03 +00:00
if adminNickname:
if path.startswith('/users/' + adminNickname + '/'):
2021-08-12 18:18:50 +00:00
isAdmin = True
2021-09-18 17:08:14 +00:00
twitterStr = \
_htmlEditProfileTwitter(baseDir, translate, removeTwitter)
# shared items section
sharesFederationStr = \
_htmlEditProfileSharedItems(baseDir, nickname,
domain, translate)
instanceStr, roleAssignStr, peertubeStr, libretranslateStr = \
_htmlEditProfileInstance(baseDir, translate,
peertubeInstances,
mediaInstanceStr,
blogsInstanceStr,
newsInstanceStr)
2020-12-24 11:56:17 +00:00
2021-07-19 19:40:04 +00:00
instanceTitle = getConfigParam(baseDir, 'instanceTitle')
2021-01-11 19:46:21 +00:00
editProfileForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
2020-11-09 22:44:03 +00:00
# keyboard navigation
2021-02-05 17:05:53 +00:00
userPathStr = '/users/' + nickname
userTimalineStr = '/users/' + nickname + '/' + defaultTimeline
2021-02-06 10:35:47 +00:00
menuTimeline = \
2021-02-06 10:46:03 +00:00
htmlHideFromScreenReader('🏠') + ' ' + \
translate['Switch to timeline view']
2021-02-06 10:35:47 +00:00
menuProfile = \
2021-02-06 10:46:03 +00:00
htmlHideFromScreenReader('👤') + ' ' + \
translate['Switch to profile view']
2021-02-05 17:05:53 +00:00
navLinks = {
2021-02-06 10:35:47 +00:00
menuProfile: userPathStr,
menuTimeline: userTimalineStr
2021-02-05 17:05:53 +00:00
}
2021-04-22 11:51:19 +00:00
navAccessKeys = {
menuProfile: 'p',
menuTimeline: 't'
}
editProfileForm += htmlKeyboardNavigation(textModeBanner,
navLinks, navAccessKeys)
# top banner
2021-02-05 15:46:05 +00:00
editProfileForm += \
_htmlEditProfileTopBanner(baseDir, nickname, domain, domainFull,
defaultTimeline, bannerFile,
path, accessKeys, translate)
2020-11-09 22:44:03 +00:00
# main info
2021-01-22 22:12:31 +00:00
editProfileForm += \
_htmlEditProfileMain(baseDir, displayNickname, bioStr,
2021-08-12 20:40:23 +00:00
movedTo, donateUrl, websiteUrl,
blogAddress, actorJson, translate)
2021-03-06 11:57:40 +00:00
# Option checkboxes
editProfileForm += \
2021-08-12 18:18:50 +00:00
_htmlEditProfileOptions(isAdmin, manuallyApprovesFollowers,
isBot, isGroup, followDMs, removeTwitter,
notifyLikes, hideLikeButton, translate)
2021-03-06 11:57:40 +00:00
# Contact information
2020-11-09 22:44:03 +00:00
editProfileForm += \
_htmlEditProfileContactInfo(nickname, emailAddress,
xmppAddress, matrixAddress,
ssbAddress, toxAddress,
briarAddress, jamiAddress,
cwtchAddress, PGPfingerprint,
PGPpubKey, translate)
2021-03-06 11:55:09 +00:00
# Customize images and banners
editProfileForm += _htmlEditProfileBackground(newsInstance, translate)
2021-03-06 11:55:09 +00:00
# Change password
editProfileForm += _htmlEditProfileChangePassword(translate)
2020-11-09 22:44:03 +00:00
2021-07-19 19:40:04 +00:00
# automatic translations
editProfileForm += libretranslateStr
2021-07-19 19:40:04 +00:00
# Filtering and blocking section
replyIntervalHours = getReplyIntervalHours(baseDir, nickname, domain,
defaultReplyIntervalHours)
2020-11-09 22:44:03 +00:00
editProfileForm += \
_htmlEditProfileFiltering(baseDir, nickname, domain,
userAgentsBlocked, translate,
replyIntervalHours)
# git projects section
2020-11-09 22:44:03 +00:00
editProfileForm += \
_htmlEditProfileGitProjects(baseDir, nickname, domain, translate)
# Skills section
2020-11-09 22:44:03 +00:00
editProfileForm += \
_htmlEditProfileSkills(baseDir, nickname, domain, translate)
editProfileForm += roleAssignStr + peertubeStr + graphicsStr
2021-09-18 17:08:14 +00:00
editProfileForm += sharesFederationStr + twitterStr + instanceStr
2021-03-06 15:18:56 +00:00
# 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'
2020-11-09 22:44:03 +00:00
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,
2021-03-14 19:22:58 +00:00
debug: bool,
buttons=[]) -> str:
2020-11-09 22:44:03 +00:00
"""An individual follow entry on the profile screen
"""
followUrlNickname = getNicknameFromActor(followUrl)
followUrlDomain, followUrlPort = getDomainFromActor(followUrl)
followUrlDomainFull = getFullDomain(followUrlDomain, followUrlPort)
titleStr = '@' + followUrlNickname + '@' + followUrlDomainFull
2020-11-09 22:44:03 +00:00
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)
2021-09-15 14:05:08 +00:00
originDomain = domain
(inboxUrl, pubKeyId, pubKey, fromPersonId, sharedInbox, avatarUrl2,
displayName, isGroup) = getPersonBox(signingPrivateKeyPem,
originDomain,
baseDir, session,
followUrlWf,
personCache, projectVersion,
httpPrefix, followUrlNickname,
domain, 'outbox', 43036)
if avatarUrl2:
avatarUrl = avatarUrl2
if displayName:
2021-02-12 17:08:48 +00:00
displayName = \
addEmojiToDisplayName(baseDir, httpPrefix,
actorNickname, domain,
displayName, False)
2021-01-12 10:23:25 +00:00
titleStr = displayName
if dormant:
titleStr += ' 💤'
2020-11-09 22:44:03 +00:00
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'
2021-07-06 10:18:56 +00:00
elif b == 'unfollow':
unfollowStr = 'Unfollow'
if isGroup:
unfollowStr = 'Leave'
2020-11-09 22:44:03 +00:00
buttonsStr += \
'<a href="/users/' + actorNickname + \
'?options=' + followUrl + \
';1;' + avatarUrl + '"><button class="buttonunfollow">' + \
translate[unfollowStr] + '</button></a>\n'
2020-11-09 22:44:03 +00:00
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