\n'
+ editNewswireForm += \
+ '
' + translate['Edit newswire'] + '
'
+ editNewswireForm += \
+ '
\n'
+
+ newswireFilename = baseDir + '/accounts/newswire.txt'
+ newswireStr = ''
+ if os.path.isfile(newswireFilename):
+ with open(newswireFilename, 'r') as fp:
+ newswireStr = fp.read()
+
+ editNewswireForm += \
+ '
'
+
+ editNewswireForm += \
+ ' ' + \
+ translate['Add RSS feed links below.'] + \
+ '
'
+ editNewswireForm += \
+ '
'
+
+ filterStr = ''
+ filterFilename = \
+ baseDir + '/accounts/news@' + domain + '/filters.txt'
+ if os.path.isfile(filterFilename):
+ with open(filterFilename, 'r') as filterfile:
+ filterStr = filterfile.read()
+
+ editNewswireForm += \
+ '
\n'
+ editNewswireForm += '
'
+ editNewswireForm += '
\n'
+
+ hashtagRulesStr = ''
+ hashtagRulesFilename = \
+ baseDir + '/accounts/hashtagrules.txt'
+ if os.path.isfile(hashtagRulesFilename):
+ with open(hashtagRulesFilename, 'r') as rulesfile:
+ hashtagRulesStr = rulesfile.read()
+
+ editNewswireForm += \
+ '
\n'
+ editNewswireForm += '
\n'
+ editNewswireForm += \
+ '
' + translate['See instructions'] + '\n'
+ editNewswireForm += '
\n'
+
+ editNewswireForm += \
+ '
'
+
+ editNewswireForm += htmlFooter()
+ return editNewswireForm
diff --git a/webapp_person_options.py b/webapp_person_options.py
new file mode 100644
index 00000000..9c37e00d
--- /dev/null
+++ b/webapp_person_options.py
@@ -0,0 +1,251 @@
+__filename__ = "webapp_person_options.py"
+__author__ = "Bob Mottram"
+__license__ = "AGPL3+"
+__version__ = "1.1.0"
+__maintainer__ = "Bob Mottram"
+__email__ = "bob@freedombone.net"
+__status__ = "Production"
+
+import os
+from shutil import copyfile
+from petnames import getPetName
+from person import isPersonSnoozed
+from posts import isModerator
+from utils import getDomainFromActor
+from utils import getNicknameFromActor
+from utils import getCSS
+from blocking import isBlocked
+from follow import isFollowingActor
+from followingCalendar import receivingCalendarEvents
+from webapp_utils import htmlHeader
+from webapp_utils import htmlFooter
+
+
+def htmlPersonOptions(cssCache: {}, translate: {}, baseDir: str,
+ domain: str, domainFull: str,
+ originPathStr: str,
+ optionsActor: str,
+ optionsProfileUrl: str,
+ optionsLink: str,
+ pageNumber: int,
+ donateUrl: str,
+ xmppAddress: str,
+ matrixAddress: str,
+ ssbAddress: str,
+ blogAddress: str,
+ toxAddress: str,
+ PGPpubKey: str,
+ PGPfingerprint: str,
+ emailAddress) -> str:
+ """Show options for a person: view/follow/block/report
+ """
+ optionsDomain, optionsPort = getDomainFromActor(optionsActor)
+ optionsDomainFull = optionsDomain
+ if optionsPort:
+ if optionsPort != 80 and optionsPort != 443:
+ optionsDomainFull = optionsDomain + ':' + str(optionsPort)
+
+ if os.path.isfile(baseDir + '/accounts/options-background-custom.jpg'):
+ if not os.path.isfile(baseDir + '/accounts/options-background.jpg'):
+ copyfile(baseDir + '/accounts/options-background.jpg',
+ baseDir + '/accounts/options-background.jpg')
+
+ followStr = 'Follow'
+ blockStr = 'Block'
+ nickname = None
+ optionsNickname = None
+ if originPathStr.startswith('/users/'):
+ nickname = originPathStr.split('/users/')[1]
+ if '/' in nickname:
+ nickname = nickname.split('/')[0]
+ if '?' in nickname:
+ nickname = nickname.split('?')[0]
+ followerDomain, followerPort = getDomainFromActor(optionsActor)
+ if isFollowingActor(baseDir, nickname, domain, optionsActor):
+ followStr = 'Unfollow'
+
+ optionsNickname = getNicknameFromActor(optionsActor)
+ optionsDomainFull = optionsDomain
+ if optionsPort:
+ if optionsPort != 80 and optionsPort != 443:
+ optionsDomainFull = optionsDomain + ':' + str(optionsPort)
+ if isBlocked(baseDir, nickname, domain,
+ optionsNickname, optionsDomainFull):
+ blockStr = 'Block'
+
+ optionsLinkStr = ''
+ if optionsLink:
+ optionsLinkStr = \
+ '
\n'
+ cssFilename = baseDir + '/epicyon-options.css'
+ if os.path.isfile(baseDir + '/options.css'):
+ cssFilename = baseDir + '/options.css'
+
+ profileStyle = getCSS(baseDir, cssFilename, cssCache)
+ if profileStyle:
+ profileStyle = \
+ profileStyle.replace('--follow-text-entry-width: 90%;',
+ '--follow-text-entry-width: 20%;')
+
+ if not os.path.isfile(baseDir + '/accounts/' +
+ 'options-background.jpg'):
+ profileStyle = \
+ profileStyle.replace('background-image: ' +
+ 'url("options-background.jpg");',
+ 'background-image: none;')
+
+ # To snooze, or not to snooze? That is the question
+ snoozeButtonStr = 'Snooze'
+ if nickname:
+ if isPersonSnoozed(baseDir, nickname, domain, optionsActor):
+ snoozeButtonStr = 'Unsnooze'
+
+ donateStr = ''
+ if donateUrl:
+ donateStr = \
+ '
\n'
+
+ optionsStr = htmlHeader(cssFilename, profileStyle)
+ optionsStr += '
\n'
+ optionsStr += '
\n'
+ optionsStr += '
\n'
+ optionsStr += '
\n'
+ optionsStr += ' \n'
+ optionsStr += ' \n'
+ handle = getNicknameFromActor(optionsActor) + '@' + optionsDomain
+ optionsStr += \
+ ' ' + translate['Options for'] + \
+ ' @' + handle + '
\n'
+ if emailAddress:
+ optionsStr += \
+ '' + translate['Email'] + \
+ ': ' + emailAddress + '
\n'
+ if xmppAddress:
+ optionsStr += \
+ '' + translate['XMPP'] + \
+ ': ' + \
+ xmppAddress + '
\n'
+ if matrixAddress:
+ optionsStr += \
+ '' + translate['Matrix'] + ': ' + \
+ matrixAddress + '
\n'
+ if ssbAddress:
+ optionsStr += \
+ 'SSB: ' + ssbAddress + '
\n'
+ if blogAddress:
+ optionsStr += \
+ 'Blog: ' + \
+ blogAddress + '
\n'
+ if toxAddress:
+ optionsStr += \
+ 'Tox: ' + toxAddress + '
\n'
+ if PGPfingerprint:
+ optionsStr += 'PGP: ' + \
+ PGPfingerprint.replace('\n', '
') + '
\n'
+ if PGPpubKey:
+ optionsStr += '' + \
+ PGPpubKey.replace('\n', '
') + '
\n'
+ optionsStr += ' \n'
+ optionsStr += '\n'
+ optionsStr += '
\n'
+ optionsStr += '
\n'
+ optionsStr += htmlFooter()
+ return optionsStr
diff --git a/webapp_post.py b/webapp_post.py
index 95fea56c..c86557ec 100644
--- a/webapp_post.py
+++ b/webapp_post.py
@@ -16,11 +16,16 @@ from cache import getPersonFromCache
from bookmarks import bookmarkedByPerson
from like import likedByPerson
from like import noOfLikes
+from follow import isFollowingActor
from posts import isEditor
from posts import postIsMuted
from posts import getPersonBox
from posts import isDM
from posts import downloadAnnounce
+from posts import populateRepliesJson
+from utils import locatePost
+from utils import loadJson
+from utils import getCSS
from utils import getCachedPostDirectory
from utils import getCachedPostFilename
from utils import getProtocolPrefixes
@@ -49,6 +54,9 @@ from webapp_utils import addEmojiToDisplayName
from webapp_utils import postContainsPublic
from webapp_utils import getContentWarningButton
from webapp_utils import getPostAttachmentsAsHtml
+from webapp_utils import getIconsDir
+from webapp_utils import htmlHeader
+from webapp_utils import htmlFooter
from webapp_media import addEmbeddedElements
from webapp_question import insertQuestion
from devices import E2EEdecryptMessageFromDevice
@@ -1215,3 +1223,162 @@ def individualPostAsHtml(allowDownloads: bool,
print('TIMING INDIV ' + boxName + ' 19 = ' + str(timeDiff))
return postHtml
+
+
+def htmlIndividualPost(cssCache: {},
+ recentPostsCache: {}, maxRecentPosts: int,
+ translate: {},
+ baseDir: str, session, wfRequest: {}, personCache: {},
+ nickname: str, domain: str, port: int, authorized: bool,
+ postJsonObject: {}, httpPrefix: str,
+ projectVersion: str, likedBy: str,
+ YTReplacementDomain: str,
+ showPublishedDateOnly: bool) -> str:
+ """Show an individual post as html
+ """
+ iconsDir = getIconsDir(baseDir)
+ postStr = ''
+ if likedBy:
+ likedByNickname = getNicknameFromActor(likedBy)
+ likedByDomain, likedByPort = getDomainFromActor(likedBy)
+ if likedByPort:
+ if likedByPort != 80 and likedByPort != 443:
+ likedByDomain += ':' + str(likedByPort)
+ likedByHandle = likedByNickname + '@' + likedByDomain
+ postStr += \
+ '
' + translate['Liked by'] + \
+ ' @' + \
+ likedByHandle + '\n'
+
+ domainFull = domain
+ if port:
+ if port != 80 and port != 443:
+ domainFull = domain + ':' + str(port)
+ actor = '/users/' + nickname
+ followStr = '
\n'
+ postStr += followStr + '\n'
+
+ postStr += \
+ individualPostAsHtml(True, recentPostsCache, maxRecentPosts,
+ iconsDir, translate, None,
+ baseDir, session, wfRequest, personCache,
+ nickname, domain, port, postJsonObject,
+ None, True, False,
+ httpPrefix, projectVersion, 'inbox',
+ YTReplacementDomain,
+ showPublishedDateOnly,
+ False, authorized, False, False, False)
+ messageId = removeIdEnding(postJsonObject['id'])
+
+ # show the previous posts
+ if isinstance(postJsonObject['object'], dict):
+ while postJsonObject['object'].get('inReplyTo'):
+ postFilename = \
+ locatePost(baseDir, nickname, domain,
+ postJsonObject['object']['inReplyTo'])
+ if not postFilename:
+ break
+ postJsonObject = loadJson(postFilename)
+ if postJsonObject:
+ postStr = \
+ individualPostAsHtml(True, recentPostsCache,
+ maxRecentPosts,
+ iconsDir, translate, None,
+ baseDir, session, wfRequest,
+ personCache,
+ nickname, domain, port,
+ postJsonObject,
+ None, True, False,
+ httpPrefix, projectVersion, 'inbox',
+ YTReplacementDomain,
+ showPublishedDateOnly,
+ False, authorized,
+ False, False, False) + postStr
+
+ # show the following posts
+ postFilename = locatePost(baseDir, nickname, domain, messageId)
+ if postFilename:
+ # is there a replies file for this post?
+ repliesFilename = postFilename.replace('.json', '.replies')
+ if os.path.isfile(repliesFilename):
+ # get items from the replies file
+ repliesJson = {
+ 'orderedItems': []
+ }
+ populateRepliesJson(baseDir, nickname, domain,
+ repliesFilename, authorized, repliesJson)
+ # add items to the html output
+ for item in repliesJson['orderedItems']:
+ postStr += \
+ individualPostAsHtml(True, recentPostsCache,
+ maxRecentPosts,
+ iconsDir, translate, None,
+ baseDir, session, wfRequest,
+ personCache,
+ nickname, domain, port, item,
+ None, True, False,
+ httpPrefix, projectVersion, 'inbox',
+ YTReplacementDomain,
+ showPublishedDateOnly,
+ False, authorized,
+ False, False, False)
+ cssFilename = baseDir + '/epicyon-profile.css'
+ if os.path.isfile(baseDir + '/epicyon.css'):
+ cssFilename = baseDir + '/epicyon.css'
+
+ postsCSS = getCSS(baseDir, cssFilename, cssCache)
+ if postsCSS:
+ if httpPrefix != 'https':
+ postsCSS = postsCSS.replace('https://',
+ httpPrefix + '://')
+ return htmlHeader(cssFilename, postsCSS) + postStr + htmlFooter()
+
+
+def htmlPostReplies(cssCache: {},
+ recentPostsCache: {}, maxRecentPosts: int,
+ translate: {}, baseDir: str,
+ session, wfRequest: {}, personCache: {},
+ nickname: str, domain: str, port: int, repliesJson: {},
+ httpPrefix: str, projectVersion: str,
+ YTReplacementDomain: str,
+ showPublishedDateOnly: bool) -> str:
+ """Show the replies to an individual post as html
+ """
+ iconsDir = getIconsDir(baseDir)
+ repliesStr = ''
+ if repliesJson.get('orderedItems'):
+ for item in repliesJson['orderedItems']:
+ repliesStr += \
+ individualPostAsHtml(True, recentPostsCache,
+ maxRecentPosts,
+ iconsDir, translate, None,
+ baseDir, session, wfRequest, personCache,
+ nickname, domain, port, item,
+ None, True, False,
+ httpPrefix, projectVersion, 'inbox',
+ YTReplacementDomain,
+ showPublishedDateOnly,
+ False, False, False, False, False)
+
+ cssFilename = baseDir + '/epicyon-profile.css'
+ if os.path.isfile(baseDir + '/epicyon.css'):
+ cssFilename = baseDir + '/epicyon.css'
+
+ postsCSS = getCSS(baseDir, cssFilename, cssCache)
+ if postsCSS:
+ if httpPrefix != 'https':
+ postsCSS = postsCSS.replace('https://',
+ httpPrefix + '://')
+ return htmlHeader(cssFilename, postsCSS) + repliesStr + htmlFooter()
diff --git a/webapp_profile.py b/webapp_profile.py
new file mode 100644
index 00000000..4af17a41
--- /dev/null
+++ b/webapp_profile.py
@@ -0,0 +1,1472 @@
+__filename__ = "webapp_profile.py"
+__author__ = "Bob Mottram"
+__license__ = "AGPL3+"
+__version__ = "1.1.0"
+__maintainer__ = "Bob Mottram"
+__email__ = "bob@freedombone.net"
+__status__ = "Production"
+
+import os
+from pprint import pprint
+from utils import getNicknameFromActor
+from utils import getDomainFromActor
+from utils import getCSS
+from utils import isSystemAccount
+from utils import removeHtml
+from utils import loadJson
+from utils import getConfigParam
+from skills import getSkills
+from theme import getThemesList
+from person import personBoxJson
+from webfinger import webfingerHandle
+from session import getJson
+from posts import parseUserFeed
+from posts import getUserUrl
+from posts import getPersonBox
+from donate import getDonationUrl
+from xmpp import getXmppAddress
+from matrix import getMatrixAddress
+from ssb import getSSBAddress
+from pgp import getEmailAddress
+from pgp import getPGPfingerprint
+from pgp import getPGPpubKey
+from tox import getToxAddress
+from webapp_utils import scheduledPostsExist
+from webapp_utils import getPersonAvatarUrl
+from webapp_utils import getIconsDir
+from webapp_utils import htmlHeader
+from webapp_utils import htmlFooter
+from webapp_utils import addEmojiToDisplayName
+from webapp_utils import headerButtonsFrontScreen
+from webapp_utils import getBannerFile
+from webapp_utils import htmlPostSeparator
+from webapp_utils import getBlogAddress
+from webapp_post import individualPostAsHtml
+from webapp_column_left import getLeftColumnContent
+from webapp_column_right import getRightColumnContent
+from webapp_timeline import htmlIndividualShare
+
+
+def htmlProfileAfterSearch(cssCache: {},
+ recentPostsCache: {}, maxRecentPosts: int,
+ translate: {},
+ baseDir: str, path: str, httpPrefix: str,
+ nickname: str, domain: str, port: int,
+ profileHandle: str,
+ session, cachedWebfingers: {}, personCache: {},
+ debug: bool, projectVersion: str,
+ YTReplacementDomain: str,
+ showPublishedDateOnly: bool) -> str:
+ """Show a profile page after a search for a fediverse address
+ """
+ if '/users/' in profileHandle or \
+ '/accounts/' in profileHandle or \
+ '/channel/' in profileHandle or \
+ '/profile/' in profileHandle or \
+ '/@' in profileHandle:
+ searchNickname = getNicknameFromActor(profileHandle)
+ searchDomain, searchPort = getDomainFromActor(profileHandle)
+ else:
+ if '@' not in profileHandle:
+ print('DEBUG: no @ in ' + profileHandle)
+ return None
+ if profileHandle.startswith('@'):
+ profileHandle = profileHandle[1:]
+ if '@' not in profileHandle:
+ print('DEBUG: no @ in ' + profileHandle)
+ return None
+ searchNickname = profileHandle.split('@')[0]
+ searchDomain = profileHandle.split('@')[1]
+ searchPort = None
+ if ':' in searchDomain:
+ searchPortStr = searchDomain.split(':')[1]
+ if searchPortStr.isdigit():
+ searchPort = int(searchPortStr)
+ searchDomain = searchDomain.split(':')[0]
+ if searchPort:
+ print('DEBUG: Search for handle ' +
+ str(searchNickname) + '@' + str(searchDomain) + ':' +
+ str(searchPort))
+ else:
+ print('DEBUG: Search for handle ' +
+ str(searchNickname) + '@' + str(searchDomain))
+ if not searchNickname:
+ print('DEBUG: No nickname found in ' + profileHandle)
+ return None
+ if not searchDomain:
+ print('DEBUG: No domain found in ' + profileHandle)
+ return None
+
+ searchDomainFull = searchDomain
+ if searchPort:
+ if searchPort != 80 and searchPort != 443:
+ if ':' not in searchDomain:
+ searchDomainFull = searchDomain + ':' + str(searchPort)
+
+ profileStr = ''
+ cssFilename = baseDir + '/epicyon-profile.css'
+ if os.path.isfile(baseDir + '/epicyon.css'):
+ cssFilename = baseDir + '/epicyon.css'
+
+ profileStyle = getCSS(baseDir, cssFilename, cssCache)
+ if profileStyle:
+ wf = \
+ webfingerHandle(session,
+ searchNickname + '@' + searchDomainFull,
+ httpPrefix, cachedWebfingers,
+ domain, projectVersion)
+ if not wf:
+ print('DEBUG: Unable to webfinger ' +
+ searchNickname + '@' + searchDomainFull)
+ print('DEBUG: cachedWebfingers ' + str(cachedWebfingers))
+ print('DEBUG: httpPrefix ' + httpPrefix)
+ print('DEBUG: domain ' + domain)
+ return None
+ if not isinstance(wf, dict):
+ print('WARN: Webfinger search for ' +
+ searchNickname + '@' + searchDomainFull +
+ ' did not return a dict. ' +
+ str(wf))
+ return None
+
+ personUrl = None
+ if wf.get('errors'):
+ personUrl = httpPrefix + '://' + \
+ searchDomainFull + '/users/' + searchNickname
+
+ profileStr = 'https://www.w3.org/ns/activitystreams'
+ asHeader = {
+ 'Accept': 'application/activity+json; profile="' + profileStr + '"'
+ }
+ if not personUrl:
+ personUrl = getUserUrl(wf)
+ if not personUrl:
+ # try single user instance
+ asHeader = {
+ 'Accept': 'application/ld+json; profile="' + profileStr + '"'
+ }
+ personUrl = httpPrefix + '://' + searchDomainFull
+ profileJson = \
+ getJson(session, personUrl, asHeader, None,
+ projectVersion, httpPrefix, domain)
+ if not profileJson:
+ asHeader = {
+ 'Accept': 'application/ld+json; profile="' + profileStr + '"'
+ }
+ profileJson = \
+ getJson(session, personUrl, asHeader, None,
+ projectVersion, httpPrefix, domain)
+ if not profileJson:
+ print('DEBUG: No actor returned from ' + personUrl)
+ return None
+ avatarUrl = ''
+ if profileJson.get('icon'):
+ if profileJson['icon'].get('url'):
+ avatarUrl = profileJson['icon']['url']
+ if not avatarUrl:
+ avatarUrl = getPersonAvatarUrl(baseDir, personUrl,
+ personCache, True)
+ displayName = searchNickname
+ if profileJson.get('name'):
+ displayName = profileJson['name']
+ profileDescription = ''
+ if profileJson.get('summary'):
+ profileDescription = profileJson['summary']
+ outboxUrl = None
+ if not profileJson.get('outbox'):
+ if debug:
+ pprint(profileJson)
+ print('DEBUG: No outbox found')
+ return None
+ outboxUrl = profileJson['outbox']
+ profileBackgroundImage = ''
+ if profileJson.get('image'):
+ if profileJson['image'].get('url'):
+ profileBackgroundImage = profileJson['image']['url']
+
+ profileStyle = profileStyle.replace('image.png',
+ profileBackgroundImage)
+ if httpPrefix != 'https':
+ profileStyle = profileStyle.replace('https://',
+ httpPrefix + '://')
+ # url to return to
+ backUrl = path
+ if not backUrl.endswith('/inbox'):
+ backUrl += '/inbox'
+
+ profileDescriptionShort = profileDescription
+ if '\n' in profileDescription:
+ if len(profileDescription.split('\n')) > 2:
+ profileDescriptionShort = ''
+ else:
+ if '
' in profileDescription:
+ if len(profileDescription.split('
')) > 2:
+ profileDescriptionShort = ''
+ # keep the profile description short
+ if len(profileDescriptionShort) > 256:
+ profileDescriptionShort = ''
+ # remove formatting from profile description used on title
+ avatarDescription = ''
+ if profileJson.get('summary'):
+ if isinstance(profileJson['summary'], str):
+ avatarDescription = \
+ profileJson['summary'].replace('
', '\n')
+ avatarDescription = avatarDescription.replace('
', '')
+ avatarDescription = avatarDescription.replace('
', '')
+ if '<' in avatarDescription:
+ avatarDescription = removeHtml(avatarDescription)
+ profileStr = '
\n'
+ profileStr += '
\n'
+ if avatarUrl:
+ profileStr += \
+ '
\n'
+ profileStr += '
' + displayName + '
\n'
+ profileStr += '
@' + searchNickname + '@' + \
+ searchDomainFull + '
\n'
+ profileStr += '
' + profileDescriptionShort + '
\n'
+ profileStr += '
\n'
+ profileStr += '
\n'
+ profileStr += '
\n'
+ profileStr += '
\n'
+ profileStr += '
\n'
+
+ iconsDir = getIconsDir(baseDir)
+ i = 0
+ for item in parseUserFeed(session, outboxUrl, asHeader,
+ projectVersion, httpPrefix, domain):
+ if not item.get('type'):
+ continue
+ if item['type'] != 'Create' and item['type'] != 'Announce':
+ continue
+ if not item.get('object'):
+ continue
+ profileStr += \
+ individualPostAsHtml(True, recentPostsCache, maxRecentPosts,
+ iconsDir, translate, None, baseDir,
+ session, cachedWebfingers, personCache,
+ nickname, domain, port,
+ item, avatarUrl, False, False,
+ httpPrefix, projectVersion, 'inbox',
+ YTReplacementDomain,
+ showPublishedDateOnly,
+ False, False, False, False, False)
+ i += 1
+ if i >= 20:
+ break
+
+ return htmlHeader(cssFilename, profileStyle) + profileStr + htmlFooter()
+
+
+def htmlProfile(rssIconAtTop: bool,
+ cssCache: {}, iconsAsButtons: bool,
+ defaultTimeline: str,
+ recentPostsCache: {}, maxRecentPosts: int,
+ translate: {}, projectVersion: str,
+ baseDir: str, httpPrefix: str, authorized: bool,
+ profileJson: {}, selected: str,
+ session, wfRequest: {}, personCache: {},
+ YTReplacementDomain: str,
+ showPublishedDateOnly: bool,
+ newswire: {}, extraJson=None,
+ pageNumber=None, maxItemsPerPage=None) -> str:
+ """Show the profile page as html
+ """
+ nickname = profileJson['preferredUsername']
+ if not nickname:
+ return ""
+ domain, port = getDomainFromActor(profileJson['id'])
+ if not domain:
+ return ""
+ displayName = \
+ addEmojiToDisplayName(baseDir, httpPrefix,
+ nickname, domain,
+ profileJson['name'], True)
+ domainFull = domain
+ if port:
+ domainFull = domain + ':' + str(port)
+ profileDescription = \
+ addEmojiToDisplayName(baseDir, httpPrefix,
+ nickname, domain,
+ profileJson['summary'], False)
+ postsButton = 'button'
+ followingButton = 'button'
+ followersButton = 'button'
+ rolesButton = 'button'
+ skillsButton = 'button'
+ sharesButton = 'button'
+ if selected == 'posts':
+ postsButton = 'buttonselected'
+ elif selected == 'following':
+ followingButton = 'buttonselected'
+ elif selected == 'followers':
+ followersButton = 'buttonselected'
+ elif selected == 'roles':
+ rolesButton = 'buttonselected'
+ elif selected == 'skills':
+ skillsButton = 'buttonselected'
+ elif selected == 'shares':
+ sharesButton = 'buttonselected'
+ loginButton = ''
+
+ followApprovalsSection = ''
+ followApprovals = False
+ linkToTimelineStart = ''
+ linkToTimelineEnd = ''
+ editProfileStr = ''
+ logoutStr = ''
+ actor = profileJson['id']
+ usersPath = '/users/' + actor.split('/users/')[1]
+
+ donateSection = ''
+ donateUrl = getDonationUrl(profileJson)
+ PGPpubKey = getPGPpubKey(profileJson)
+ PGPfingerprint = getPGPfingerprint(profileJson)
+ emailAddress = getEmailAddress(profileJson)
+ xmppAddress = getXmppAddress(profileJson)
+ matrixAddress = getMatrixAddress(profileJson)
+ ssbAddress = getSSBAddress(profileJson)
+ toxAddress = getToxAddress(profileJson)
+ if donateUrl or xmppAddress or matrixAddress or \
+ ssbAddress or toxAddress or PGPpubKey or \
+ PGPfingerprint or emailAddress:
+ donateSection = '
\n'
+ donateSection += '
\n'
+ if donateUrl and not isSystemAccount(nickname):
+ donateSection += \
+ '
\n'
+ if emailAddress:
+ donateSection += \
+ '' + translate['Email'] + ': ' + emailAddress + '
\n'
+ if xmppAddress:
+ donateSection += \
+ '' + translate['XMPP'] + ': '+xmppAddress + '
\n'
+ if matrixAddress:
+ donateSection += \
+ '' + translate['Matrix'] + ': ' + matrixAddress + '
\n'
+ if ssbAddress:
+ donateSection += \
+ 'SSB:
\n'
+ if toxAddress:
+ donateSection += \
+ 'Tox:
\n'
+ if PGPfingerprint:
+ donateSection += \
+ 'PGP: ' + \
+ PGPfingerprint.replace('\n', '
') + '
\n'
+ if PGPpubKey:
+ donateSection += \
+ '' + PGPpubKey.replace('\n', '
') + '
\n'
+ donateSection += ' \n'
+ donateSection += '
\n'
+
+ iconsDir = getIconsDir(baseDir)
+ if not authorized:
+ loginButton = headerButtonsFrontScreen(translate, nickname,
+ 'features', authorized,
+ iconsAsButtons, iconsDir)
+ else:
+ editProfileStr = \
+ '
' + \
+ '\n'
+
+ logoutStr = \
+ '
' + \
+ '\n'
+
+ linkToTimelineStart = \
+ '
'
+ linkToTimelineStart += \
+ '
'
+ linkToTimelineEnd = ''
+ # are there any follow requests?
+ followRequestsFilename = \
+ baseDir + '/accounts/' + \
+ nickname + '@' + domain + '/followrequests.txt'
+ if os.path.isfile(followRequestsFilename):
+ with open(followRequestsFilename, 'r') as f:
+ for line in f:
+ if len(line) > 0:
+ followApprovals = True
+ followersButton = 'buttonhighlighted'
+ if selected == 'followers':
+ followersButton = 'buttonselectedhighlighted'
+ break
+ if selected == 'followers':
+ if followApprovals:
+ with open(followRequestsFilename, 'r') as f:
+ for followerHandle in f:
+ if len(line) > 0:
+ if '://' in followerHandle:
+ followerActor = followerHandle
+ else:
+ followerActor = \
+ httpPrefix + '://' + \
+ followerHandle.split('@')[1] + \
+ '/users/' + followerHandle.split('@')[0]
+ basePath = '/users/' + nickname
+ followApprovalsSection += '
'
+
+ profileDescriptionShort = profileDescription
+ if '\n' in profileDescription:
+ if len(profileDescription.split('\n')) > 2:
+ profileDescriptionShort = ''
+ else:
+ if '
' in profileDescription:
+ if len(profileDescription.split('
')) > 2:
+ profileDescriptionShort = ''
+ profileDescription = profileDescription.replace('
', '\n')
+ # keep the profile description short
+ if len(profileDescriptionShort) > 256:
+ profileDescriptionShort = ''
+ # remove formatting from profile description used on title
+ avatarDescription = ''
+ if profileJson.get('summary'):
+ avatarDescription = profileJson['summary'].replace('
', '\n')
+ avatarDescription = avatarDescription.replace('
', '')
+ avatarDescription = avatarDescription.replace('
', '')
+
+ # If this is the news account then show a different banner
+ if isSystemAccount(nickname):
+ bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain)
+ profileHeaderStr = \
+ '
\n'
+ if loginButton:
+ profileHeaderStr += '
' + loginButton + '\n'
+
+ profileHeaderStr += '
\n'
+ profileHeaderStr += ' \n'
+ profileHeaderStr += ' \n'
+ profileHeaderStr += ' \n'
+ profileHeaderStr += ' \n'
+ profileHeaderStr += ' \n'
+ profileHeaderStr += ' \n'
+ profileHeaderStr += ' \n'
+ profileHeaderStr += ' \n'
+ iconsDir = getIconsDir(baseDir)
+ profileHeaderStr += \
+ getLeftColumnContent(baseDir, 'news', domainFull,
+ httpPrefix, translate,
+ iconsDir, False,
+ False, None, rssIconAtTop, True,
+ True)
+ profileHeaderStr += ' | \n'
+ profileHeaderStr += ' \n'
+ else:
+ profileHeaderStr = '\n'
+ profileHeaderStr += ' \n'
+ profileHeaderStr += \
+ ' \n'
+ profileHeaderStr += ' ' + displayName + '\n'
+ iconsDir = getIconsDir(baseDir)
+ profileHeaderStr += \
+ ' @' + nickname + '@' + domainFull + ' '
+ profileHeaderStr += \
+ '' + \
+ ' \n'
+ profileHeaderStr += ' ' + profileDescriptionShort + ' \n'
+ profileHeaderStr += loginButton
+ profileHeaderStr += ' \n'
+ profileHeaderStr += ' \n'
+
+ profileStr = \
+ linkToTimelineStart + profileHeaderStr + \
+ linkToTimelineEnd + donateSection
+ if not isSystemAccount(nickname):
+ profileStr += ''
+
+ profileStr += followApprovalsSection
+
+ cssFilename = baseDir + '/epicyon-profile.css'
+ if os.path.isfile(baseDir + '/epicyon.css'):
+ cssFilename = baseDir + '/epicyon.css'
+
+ profileStyle = getCSS(baseDir, cssFilename, cssCache)
+ if profileStyle:
+ profileStyle = \
+ profileStyle.replace('image.png',
+ profileJson['image']['url'])
+ if isSystemAccount(nickname):
+ bannerFile, bannerFilename = \
+ getBannerFile(baseDir, nickname, domain)
+ profileStyle = \
+ profileStyle.replace('banner.png',
+ '/users/' + nickname + '/' + bannerFile)
+
+ licenseStr = \
+ '' + \
+ ''
+
+ if selected == 'posts':
+ profileStr += \
+ htmlProfilePosts(recentPostsCache, maxRecentPosts,
+ translate,
+ baseDir, httpPrefix, authorized,
+ nickname, domain, port,
+ session, wfRequest, personCache,
+ projectVersion,
+ YTReplacementDomain,
+ showPublishedDateOnly) + licenseStr
+ if selected == 'following':
+ profileStr += \
+ htmlProfileFollowing(translate, baseDir, httpPrefix,
+ authorized, nickname,
+ domain, port, session,
+ wfRequest, personCache, extraJson,
+ projectVersion, ["unfollow"], selected,
+ usersPath, pageNumber, maxItemsPerPage)
+ if selected == 'followers':
+ profileStr += \
+ htmlProfileFollowing(translate, baseDir, httpPrefix,
+ authorized, nickname,
+ domain, port, session,
+ wfRequest, personCache, extraJson,
+ projectVersion, ["block"],
+ selected, usersPath, pageNumber,
+ maxItemsPerPage)
+ if selected == 'roles':
+ profileStr += \
+ htmlProfileRoles(translate, nickname, domainFull, extraJson)
+ if selected == 'skills':
+ profileStr += \
+ htmlProfileSkills(translate, nickname, domainFull, extraJson)
+ if selected == 'shares':
+ profileStr += \
+ htmlProfileShares(actor, translate,
+ nickname, domainFull,
+ extraJson) + licenseStr
+
+ # Footer which is only used for system accounts
+ profileFooterStr = ''
+ if isSystemAccount(nickname):
+ profileFooterStr = ' | \n'
+ profileFooterStr += ' \n'
+ iconsDir = getIconsDir(baseDir)
+ profileFooterStr += \
+ getRightColumnContent(baseDir, 'news', domainFull,
+ httpPrefix, translate,
+ iconsDir, False, False,
+ newswire, False,
+ False, None, False, False,
+ False, True, authorized, True)
+ profileFooterStr += ' | \n'
+ profileFooterStr += '
\n'
+ profileFooterStr += ' \n'
+ profileFooterStr += '
\n'
+
+ profileStr = \
+ htmlHeader(cssFilename, profileStyle) + \
+ profileStr + profileFooterStr + htmlFooter()
+ return profileStr
+
+
+def htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int,
+ translate: {},
+ baseDir: str, httpPrefix: str,
+ authorized: bool,
+ nickname: str, domain: str, port: int,
+ session, wfRequest: {}, personCache: {},
+ projectVersion: str,
+ YTReplacementDomain: str,
+ showPublishedDateOnly: bool) -> str:
+ """Shows posts on the profile screen
+ These should only be public posts
+ """
+ iconsDir = getIconsDir(baseDir)
+ separatorStr = htmlPostSeparator(baseDir, None)
+ profileStr = ''
+ maxItems = 4
+ ctr = 0
+ currPage = 1
+ while ctr < maxItems and currPage < 4:
+ outboxFeed = \
+ personBoxJson({}, session, baseDir, domain,
+ port,
+ '/users/' + nickname + '/outbox?page=' +
+ str(currPage),
+ httpPrefix,
+ 10, 'outbox',
+ authorized, 0, False, 0)
+ if not outboxFeed:
+ break
+ if len(outboxFeed['orderedItems']) == 0:
+ break
+ for item in outboxFeed['orderedItems']:
+ if item['type'] == 'Create':
+ postStr = \
+ individualPostAsHtml(True, recentPostsCache,
+ maxRecentPosts,
+ iconsDir, translate, None,
+ baseDir, session, wfRequest,
+ personCache,
+ nickname, domain, port, item,
+ None, True, False,
+ httpPrefix, projectVersion, 'inbox',
+ YTReplacementDomain,
+ showPublishedDateOnly,
+ False, False, False, True, False)
+ if postStr:
+ profileStr += separatorStr + postStr
+ ctr += 1
+ if ctr >= maxItems:
+ break
+ currPage += 1
+ return profileStr
+
+
+def htmlProfileFollowing(translate: {}, baseDir: str, httpPrefix: str,
+ authorized: bool,
+ nickname: str, domain: str, port: int,
+ session, wfRequest: {}, personCache: {},
+ followingJson: {}, projectVersion: str,
+ buttons: [],
+ feedName: str, actor: str,
+ pageNumber: int,
+ maxItemsPerPage: int) -> str:
+ """Shows following on the profile screen
+ """
+ profileStr = ''
+
+ iconsDir = getIconsDir(baseDir)
+ if authorized and pageNumber:
+ if authorized and pageNumber > 1:
+ # page up arrow
+ profileStr += \
+ '
\n' + \
+ ' \n' + \
+ ' \n'
+
+ for item in followingJson['orderedItems']:
+ profileStr += \
+ individualFollowAsHtml(translate, baseDir, session,
+ wfRequest, personCache,
+ domain, item, authorized, nickname,
+ httpPrefix, projectVersion,
+ buttons)
+ if authorized and maxItemsPerPage and pageNumber:
+ if len(followingJson['orderedItems']) >= maxItemsPerPage:
+ # page down arrow
+ profileStr += \
+ '
\n' + \
+ ' \n' + \
+ ' \n'
+ return profileStr
+
+
+def htmlProfileRoles(translate: {}, nickname: str, domain: str,
+ rolesJson: {}) -> str:
+ """Shows roles on the profile screen
+ """
+ profileStr = ''
+ for project, rolesList in rolesJson.items():
+ profileStr += \
+ '
\n
' + project + \
+ '
\n
\n'
+ for role in rolesList:
+ profileStr += '
' + role + '
\n'
+ profileStr += '\n'
+ if len(profileStr) == 0:
+ profileStr += \
+ '
@' + nickname + '@' + domain + ' has no roles assigned
\n'
+ else:
+ profileStr = '
' + profileStr + '
\n'
+ return profileStr
+
+
+def htmlProfileSkills(translate: {}, nickname: str, domain: str,
+ skillsJson: {}) -> str:
+ """Shows skills on the profile screen
+ """
+ profileStr = ''
+ for skill, level in skillsJson.items():
+ profileStr += \
+ '
\n
\n'
+ if len(profileStr) > 0:
+ profileStr = '
' + \
+ profileStr + '
\n'
+ return profileStr
+
+
+def htmlProfileShares(actor: str, translate: {},
+ nickname: str, domain: str, sharesJson: {}) -> str:
+ """Shows shares on the profile screen
+ """
+ profileStr = ''
+ for item in sharesJson['orderedItems']:
+ profileStr += htmlIndividualShare(actor, item, translate, False, False)
+ if len(profileStr) > 0:
+ profileStr = '
' + profileStr + '
\n'
+ return profileStr
+
+
+def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str,
+ domain: str, port: int, httpPrefix: str,
+ defaultTimeline: str) -> str:
+ """Shows the edit profile screen
+ """
+ imageFormats = '.png, .jpg, .jpeg, .gif, .webp, .avif'
+ path = path.replace('/inbox', '').replace('/outbox', '')
+ path = path.replace('/shares', '')
+ nickname = getNicknameFromActor(path)
+ if not nickname:
+ return ''
+ domainFull = domain
+ if port:
+ if port != 80 and port != 443:
+ if ':' not in domain:
+ domainFull = domain + ':' + str(port)
+
+ actorFilename = \
+ baseDir + '/accounts/' + nickname + '@' + domain + '.json'
+ if not os.path.isfile(actorFilename):
+ return ''
+
+ # filename of the banner shown at the top
+ bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain)
+
+ isBot = ''
+ isGroup = ''
+ followDMs = ''
+ removeTwitter = ''
+ notifyLikes = ''
+ hideLikeButton = ''
+ mediaInstanceStr = ''
+ blogsInstanceStr = ''
+ newsInstanceStr = ''
+ displayNickname = nickname
+ bioStr = ''
+ donateUrl = ''
+ emailAddress = ''
+ PGPpubKey = ''
+ PGPfingerprint = ''
+ xmppAddress = ''
+ matrixAddress = ''
+ ssbAddress = ''
+ blogAddress = ''
+ toxAddress = ''
+ manuallyApprovesFollowers = ''
+ actorJson = loadJson(actorFilename)
+ if actorJson:
+ donateUrl = getDonationUrl(actorJson)
+ xmppAddress = getXmppAddress(actorJson)
+ matrixAddress = getMatrixAddress(actorJson)
+ ssbAddress = getSSBAddress(actorJson)
+ blogAddress = getBlogAddress(actorJson)
+ toxAddress = getToxAddress(actorJson)
+ emailAddress = getEmailAddress(actorJson)
+ PGPpubKey = getPGPpubKey(actorJson)
+ PGPfingerprint = getPGPfingerprint(actorJson)
+ if actorJson.get('name'):
+ displayNickname = actorJson['name']
+ if actorJson.get('summary'):
+ bioStr = \
+ actorJson['summary'].replace('
', '').replace('
', '')
+ if actorJson.get('manuallyApprovesFollowers'):
+ if actorJson['manuallyApprovesFollowers']:
+ manuallyApprovesFollowers = 'checked'
+ else:
+ manuallyApprovesFollowers = ''
+ if actorJson.get('type'):
+ if actorJson['type'] == 'Service':
+ isBot = 'checked'
+ isGroup = ''
+ elif actorJson['type'] == 'Group':
+ isGroup = 'checked'
+ isBot = ''
+ if os.path.isfile(baseDir + '/accounts/' +
+ nickname + '@' + domain + '/.followDMs'):
+ followDMs = 'checked'
+ if os.path.isfile(baseDir + '/accounts/' +
+ nickname + '@' + domain + '/.removeTwitter'):
+ removeTwitter = 'checked'
+ if os.path.isfile(baseDir + '/accounts/' +
+ nickname + '@' + domain + '/.notifyLikes'):
+ notifyLikes = 'checked'
+ if os.path.isfile(baseDir + '/accounts/' +
+ nickname + '@' + domain + '/.hideLikeButton'):
+ hideLikeButton = 'checked'
+
+ mediaInstance = getConfigParam(baseDir, "mediaInstance")
+ if mediaInstance:
+ if mediaInstance is True:
+ mediaInstanceStr = 'checked'
+ blogsInstanceStr = ''
+ newsInstanceStr = ''
+
+ newsInstance = getConfigParam(baseDir, "newsInstance")
+ if newsInstance:
+ if newsInstance is True:
+ newsInstanceStr = 'checked'
+ blogsInstanceStr = ''
+ mediaInstanceStr = ''
+
+ blogsInstance = getConfigParam(baseDir, "blogsInstance")
+ if blogsInstance:
+ if blogsInstance is True:
+ blogsInstanceStr = 'checked'
+ mediaInstanceStr = ''
+ newsInstanceStr = ''
+
+ filterStr = ''
+ filterFilename = \
+ baseDir + '/accounts/' + nickname + '@' + domain + '/filters.txt'
+ if os.path.isfile(filterFilename):
+ with open(filterFilename, 'r') as filterfile:
+ filterStr = filterfile.read()
+
+ switchStr = ''
+ switchFilename = \
+ baseDir + '/accounts/' + \
+ nickname + '@' + domain + '/replacewords.txt'
+ if os.path.isfile(switchFilename):
+ with open(switchFilename, 'r') as switchfile:
+ switchStr = switchfile.read()
+
+ autoTags = ''
+ autoTagsFilename = \
+ baseDir + '/accounts/' + \
+ nickname + '@' + domain + '/autotags.txt'
+ if os.path.isfile(autoTagsFilename):
+ with open(autoTagsFilename, 'r') as autoTagsFile:
+ autoTags = autoTagsFile.read()
+
+ autoCW = ''
+ autoCWFilename = \
+ baseDir + '/accounts/' + \
+ nickname + '@' + domain + '/autocw.txt'
+ if os.path.isfile(autoCWFilename):
+ with open(autoCWFilename, 'r') as autoCWFile:
+ autoCW = autoCWFile.read()
+
+ blockedStr = ''
+ blockedFilename = \
+ baseDir + '/accounts/' + \
+ nickname + '@' + domain + '/blocking.txt'
+ if os.path.isfile(blockedFilename):
+ with open(blockedFilename, 'r') as blockedfile:
+ blockedStr = blockedfile.read()
+
+ allowedInstancesStr = ''
+ allowedInstancesFilename = \
+ baseDir + '/accounts/' + \
+ nickname + '@' + domain + '/allowedinstances.txt'
+ if os.path.isfile(allowedInstancesFilename):
+ with open(allowedInstancesFilename, 'r') as allowedInstancesFile:
+ allowedInstancesStr = allowedInstancesFile.read()
+
+ gitProjectsStr = ''
+ gitProjectsFilename = \
+ baseDir + '/accounts/' + \
+ nickname + '@' + domain + '/gitprojects.txt'
+ if os.path.isfile(gitProjectsFilename):
+ with open(gitProjectsFilename, 'r') as gitProjectsFile:
+ gitProjectsStr = gitProjectsFile.read()
+
+ skills = getSkills(baseDir, nickname, domain)
+ skillsStr = ''
+ skillCtr = 1
+ if skills:
+ for skillDesc, skillValue in skills.items():
+ skillsStr += \
+ '
'
+ skillsStr += \
+ '
'
+ skillCtr += 1
+
+ skillsStr += \
+ '
'
+ skillsStr += \
+ '
'
+
+ cssFilename = baseDir + '/epicyon-profile.css'
+ if os.path.isfile(baseDir + '/epicyon.css'):
+ cssFilename = baseDir + '/epicyon.css'
+
+ editProfileCSS = getCSS(baseDir, cssFilename, cssCache)
+ if editProfileCSS:
+ if httpPrefix != 'https':
+ editProfileCSS = \
+ editProfileCSS.replace('https://', httpPrefix + '://')
+
+ moderatorsStr = ''
+ themesDropdown = ''
+ instanceStr = ''
+
+ adminNickname = getConfigParam(baseDir, 'admin')
+ if adminNickname:
+ if path.startswith('/users/' + adminNickname + '/'):
+ instanceDescription = \
+ getConfigParam(baseDir, 'instanceDescription')
+ instanceDescriptionShort = \
+ getConfigParam(baseDir, 'instanceDescriptionShort')
+ instanceTitle = \
+ getConfigParam(baseDir, 'instanceTitle')
+ instanceStr += '
'
+ instanceStr += \
+ ' '
+ if instanceTitle:
+ instanceStr += \
+ '
'
+ else:
+ instanceStr += \
+ '
'
+ instanceStr += \
+ ' '
+ if instanceDescriptionShort:
+ instanceStr += \
+ '
'
+ else:
+ instanceStr += \
+ '
'
+ instanceStr += \
+ ' '
+ if instanceDescription:
+ instanceStr += \
+ ' '
+ else:
+ instanceStr += \
+ ' '
+ instanceStr += \
+ ' '
+ instanceStr += \
+ ' '
+ instanceStr += '
'
+
+ moderators = ''
+ moderatorsFile = baseDir + '/accounts/moderators.txt'
+ if os.path.isfile(moderatorsFile):
+ with open(moderatorsFile, "r") as f:
+ moderators = f.read()
+ moderatorsStr = '
'
+ moderatorsStr += ' ' + translate['Moderators'] + '
'
+ moderatorsStr += ' ' + \
+ translate['A list of moderator nicknames. One per line.']
+ moderatorsStr += \
+ ' '
+ moderatorsStr += '
'
+
+ editors = ''
+ editorsFile = baseDir + '/accounts/editors.txt'
+ if os.path.isfile(editorsFile):
+ with open(editorsFile, "r") as f:
+ editors = f.read()
+ editorsStr = '
'
+ editorsStr += ' ' + translate['Site Editors'] + '
'
+ editorsStr += ' ' + \
+ translate['A list of editor nicknames. One per line.']
+ editorsStr += \
+ ' '
+ editorsStr += '
'
+
+ themes = getThemesList()
+ themesDropdown = '
'
+ themesDropdown += ' ' + translate['Theme'] + '
'
+ grayscaleFilename = \
+ baseDir + '/accounts/.grayscale'
+ grayscale = ''
+ if os.path.isfile(grayscaleFilename):
+ grayscale = 'checked'
+ themesDropdown += \
+ ' ' + translate['Grayscale'] + '
'
+ themesDropdown += '
'
+ if os.path.isfile(baseDir + '/fonts/custom.woff') or \
+ os.path.isfile(baseDir + '/fonts/custom.woff2') or \
+ os.path.isfile(baseDir + '/fonts/custom.otf') or \
+ os.path.isfile(baseDir + '/fonts/custom.ttf'):
+ themesDropdown += \
+ ' ' + \
+ translate['Remove the custom font'] + '
'
+ themesDropdown += '
'
+ themeName = getConfigParam(baseDir, 'theme')
+ themesDropdown = \
+ themesDropdown.replace('
\n'
+ return tlStr
+
+
+def htmlTimeline(cssCache: {}, defaultTimeline: str,
+ recentPostsCache: {}, maxRecentPosts: int,
+ translate: {}, pageNumber: int,
+ itemsPerPage: int, session, baseDir: str,
+ wfRequest: {}, personCache: {},
+ nickname: str, domain: str, port: int, timelineJson: {},
+ boxName: str, allowDeletion: bool,
+ httpPrefix: str, projectVersion: str,
+ manuallyApproveFollowers: bool,
+ minimal: bool,
+ YTReplacementDomain: str,
+ showPublishedDateOnly: bool,
+ newswire: {}, moderator: bool,
+ editor: bool,
+ positiveVoting: bool,
+ showPublishAsIcon: bool,
+ fullWidthTimelineButtonHeader: bool,
+ iconsAsButtons: bool,
+ rssIconAtTop: bool,
+ publishButtonAtTop: bool,
+ authorized: bool) -> str:
+ """Show the timeline as html
+ """
+ timelineStartTime = time.time()
+
+ accountDir = baseDir + '/accounts/' + nickname + '@' + domain
+
+ # should the calendar icon be highlighted?
+ newCalendarEvent = False
+ calendarImage = 'calendar.png'
+ calendarPath = '/calendar'
+ calendarFile = accountDir + '/.newCalendar'
+ if os.path.isfile(calendarFile):
+ newCalendarEvent = True
+ calendarImage = 'calendar_notify.png'
+ with open(calendarFile, 'r') as calfile:
+ calendarPath = calfile.read().replace('##sent##', '')
+ calendarPath = calendarPath.replace('\n', '').replace('\r', '')
+
+ # should the DM button be highlighted?
+ newDM = False
+ dmFile = accountDir + '/.newDM'
+ if os.path.isfile(dmFile):
+ newDM = True
+ if boxName == 'dm':
+ os.remove(dmFile)
+
+ # should the Replies button be highlighted?
+ newReply = False
+ replyFile = accountDir + '/.newReply'
+ if os.path.isfile(replyFile):
+ newReply = True
+ if boxName == 'tlreplies':
+ os.remove(replyFile)
+
+ # should the Shares button be highlighted?
+ newShare = False
+ newShareFile = accountDir + '/.newShare'
+ if os.path.isfile(newShareFile):
+ newShare = True
+ if boxName == 'tlshares':
+ os.remove(newShareFile)
+
+ # should the Moderation/reports button be highlighted?
+ newReport = False
+ newReportFile = accountDir + '/.newReport'
+ if os.path.isfile(newReportFile):
+ newReport = True
+ if boxName == 'moderation':
+ os.remove(newReportFile)
+
+ # directory where icons are found
+ # This changes depending upon theme
+ iconsDir = getIconsDir(baseDir)
+
+ separatorStr = ''
+ if boxName != 'tlmedia':
+ separatorStr = htmlPostSeparator(baseDir, None)
+
+ # the css filename
+ cssFilename = baseDir + '/epicyon-profile.css'
+ if os.path.isfile(baseDir + '/epicyon.css'):
+ cssFilename = baseDir + '/epicyon.css'
+
+ # filename of the banner shown at the top
+ bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain)
+
+ # benchmark 1
+ timeDiff = int((time.time() - timelineStartTime) * 1000)
+ if timeDiff > 100:
+ print('TIMELINE TIMING ' + boxName + ' 1 = ' + str(timeDiff))
+
+ profileStyle = getCSS(baseDir, cssFilename, cssCache)
+ if not profileStyle:
+ print('ERROR: css file not found ' + cssFilename)
+ return None
+
+ # replace any https within the css with whatever prefix is needed
+ if httpPrefix != 'https':
+ profileStyle = \
+ profileStyle.replace('https://',
+ httpPrefix + '://')
+
+ # is the user a moderator?
+ if not moderator:
+ moderator = isModerator(baseDir, nickname)
+
+ # is the user a site editor?
+ if not editor:
+ editor = isEditor(baseDir, nickname)
+
+ # benchmark 2
+ timeDiff = int((time.time() - timelineStartTime) * 1000)
+ if timeDiff > 100:
+ print('TIMELINE TIMING ' + boxName + ' 2 = ' + str(timeDiff))
+
+ # the appearance of buttons - highlighted or not
+ inboxButton = 'button'
+ blogsButton = 'button'
+ newsButton = 'button'
+ dmButton = 'button'
+ if newDM:
+ dmButton = 'buttonhighlighted'
+ repliesButton = 'button'
+ if newReply:
+ repliesButton = 'buttonhighlighted'
+ mediaButton = 'button'
+ bookmarksButton = 'button'
+ eventsButton = 'button'
+ sentButton = 'button'
+ sharesButton = 'button'
+ if newShare:
+ sharesButton = 'buttonhighlighted'
+ moderationButton = 'button'
+ if newReport:
+ moderationButton = 'buttonhighlighted'
+ if boxName == 'inbox':
+ inboxButton = 'buttonselected'
+ elif boxName == 'tlblogs':
+ blogsButton = 'buttonselected'
+ elif boxName == 'tlnews':
+ newsButton = 'buttonselected'
+ elif boxName == 'dm':
+ dmButton = 'buttonselected'
+ if newDM:
+ dmButton = 'buttonselectedhighlighted'
+ elif boxName == 'tlreplies':
+ repliesButton = 'buttonselected'
+ if newReply:
+ repliesButton = 'buttonselectedhighlighted'
+ elif boxName == 'tlmedia':
+ mediaButton = 'buttonselected'
+ elif boxName == 'outbox':
+ sentButton = 'buttonselected'
+ elif boxName == 'moderation':
+ moderationButton = 'buttonselected'
+ if newReport:
+ moderationButton = 'buttonselectedhighlighted'
+ elif boxName == 'tlshares':
+ sharesButton = 'buttonselected'
+ if newShare:
+ sharesButton = 'buttonselectedhighlighted'
+ elif boxName == 'tlbookmarks' or boxName == 'bookmarks':
+ bookmarksButton = 'buttonselected'
+ elif boxName == 'tlevents':
+ eventsButton = 'buttonselected'
+
+ # get the full domain, including any port number
+ fullDomain = domain
+ if port != 80 and port != 443:
+ if ':' not in domain:
+ fullDomain = domain + ':' + str(port)
+
+ usersPath = '/users/' + nickname
+ actor = httpPrefix + '://' + fullDomain + usersPath
+
+ showIndividualPostIcons = True
+
+ # show an icon for new follow approvals
+ followApprovals = ''
+ followRequestsFilename = \
+ baseDir + '/accounts/' + \
+ nickname + '@' + domain + '/followrequests.txt'
+ if os.path.isfile(followRequestsFilename):
+ with open(followRequestsFilename, 'r') as f:
+ for line in f:
+ if len(line) > 0:
+ # show follow approvals icon
+ followApprovals = \
+ '