'
editNewsPostForm += htmlFooter()
return editNewsPostForm
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('
', '')
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 += \
'
'
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 += '
'
moderators = ''
moderatorsFile = baseDir + '/accounts/moderators.txt'
if os.path.isfile(moderatorsFile):
with open(moderatorsFile, "r") as f:
moderators = f.read()
moderatorsStr = '
'
editors = ''
editorsFile = baseDir + '/accounts/editors.txt'
if os.path.isfile(editorsFile):
with open(editorsFile, "r") as f:
editors = f.read()
editorsStr = '
'
themeName = getConfigParam(baseDir, 'theme')
themesDropdown = \
themesDropdown.replace('
')
editProfileForm = htmlHeader(cssFilename, editProfileCSS)
# top banner
editProfileForm += \
'\n'
editProfileForm += ' \n'
editProfileForm += \
'\n'
editProfileForm += htmlFooter()
return editProfileForm
def htmlGetLoginCredentials(loginParams: str,
lastLoginTime: int) -> (str, str, bool):
"""Receives login credentials via HTTPServer POST
"""
if not loginParams.startswith('username='):
return None, None, None
# minimum time between login attempts
currTime = int(time.time())
if currTime < lastLoginTime+10:
return None, None, None
if '&' not in loginParams:
return None, None, None
loginArgs = loginParams.split('&')
nickname = None
password = None
register = False
for arg in loginArgs:
if '=' in arg:
if arg.split('=', 1)[0] == 'username':
nickname = arg.split('=', 1)[1]
elif arg.split('=', 1)[0] == 'password':
password = arg.split('=', 1)[1]
elif arg.split('=', 1)[0] == 'register':
register = True
return nickname, password, register
def htmlLogin(cssCache: {}, translate: {},
baseDir: str, autocomplete=True) -> str:
"""Shows the login screen
"""
accounts = noOfAccounts(baseDir)
loginImage = 'login.png'
loginImageFilename = None
if os.path.isfile(baseDir + '/accounts/' + loginImage):
loginImageFilename = baseDir + '/accounts/' + loginImage
elif os.path.isfile(baseDir + '/accounts/login.jpg'):
loginImage = 'login.jpg'
loginImageFilename = baseDir + '/accounts/' + loginImage
elif os.path.isfile(baseDir + '/accounts/login.jpeg'):
loginImage = 'login.jpeg'
loginImageFilename = baseDir + '/accounts/' + loginImage
elif os.path.isfile(baseDir + '/accounts/login.gif'):
loginImage = 'login.gif'
loginImageFilename = baseDir + '/accounts/' + loginImage
elif os.path.isfile(baseDir + '/accounts/login.webp'):
loginImage = 'login.webp'
loginImageFilename = baseDir + '/accounts/' + loginImage
elif os.path.isfile(baseDir + '/accounts/login.avif'):
loginImage = 'login.avif'
loginImageFilename = baseDir + '/accounts/' + loginImage
if not loginImageFilename:
loginImageFilename = baseDir + '/accounts/' + loginImage
copyfile(baseDir + '/img/login.png', loginImageFilename)
if os.path.isfile(baseDir + '/accounts/login-background-custom.jpg'):
if not os.path.isfile(baseDir + '/accounts/login-background.jpg'):
copyfile(baseDir + '/accounts/login-background-custom.jpg',
baseDir + '/accounts/login-background.jpg')
if accounts > 0:
loginText = \
'' + \
translate['Welcome. Please enter your login details below.'] + \
'
'
else:
loginText = \
'' + \
translate['Please enter some credentials'] + '
'
loginText += \
'' + \
translate['You will become the admin of this site.'] + \
'
'
if os.path.isfile(baseDir + '/accounts/login.txt'):
# custom login message
with open(baseDir + '/accounts/login.txt', 'r') as file:
loginText = '' + file.read() + '
'
cssFilename = baseDir + '/epicyon-login.css'
if os.path.isfile(baseDir + '/login.css'):
cssFilename = baseDir + '/login.css'
loginCSS = getCSS(baseDir, cssFilename, cssCache)
if not loginCSS:
print('ERROR: login css file missing ' + cssFilename)
return None
# show the register button
registerButtonStr = ''
if getConfigParam(baseDir, 'registration') == 'open':
if int(getConfigParam(baseDir, 'registrationsRemaining')) > 0:
if accounts > 0:
idx = 'Welcome. Please login or register a new account.'
loginText = \
'' + \
translate[idx] + \
'
'
registerButtonStr = \
'Register '
TOSstr = \
'' + \
translate['Terms of Service'] + '
'
TOSstr += \
'' + \
translate['About this Instance'] + '
'
loginButtonStr = ''
if accounts > 0:
loginButtonStr = \
'' + \
translate['Login'] + ' '
autocompleteStr = ''
if not autocomplete:
autocompleteStr = 'autocomplete="off" value=""'
loginForm = htmlHeader(cssFilename, loginCSS)
loginForm += ' \n'
loginForm += '\n'
loginForm += ' \n'
loginForm += \
'
\n'
loginForm += loginText + TOSstr + '\n'
loginForm += '
\n'
loginForm += '\n'
loginForm += ' \n'
loginForm += ' ' + \
translate['Nickname'] + ' \n'
loginForm += \
' \n'
loginForm += '\n'
loginForm += ' ' + \
translate['Password'] + ' \n'
loginForm += \
' \n'
loginForm += loginButtonStr + registerButtonStr + '\n'
loginForm += '
\n'
loginForm += ' \n'
loginForm += \
'' + \
' \n'
loginForm += htmlFooter()
return loginForm
def htmlTermsOfService(cssCache: {}, baseDir: str,
httpPrefix: str, domainFull: str) -> str:
"""Show the terms of service screen
"""
adminNickname = getConfigParam(baseDir, 'admin')
if not os.path.isfile(baseDir + '/accounts/tos.txt'):
copyfile(baseDir + '/default_tos.txt',
baseDir + '/accounts/tos.txt')
if os.path.isfile(baseDir + '/accounts/login-background-custom.jpg'):
if not os.path.isfile(baseDir + '/accounts/login-background.jpg'):
copyfile(baseDir + '/accounts/login-background-custom.jpg',
baseDir + '/accounts/login-background.jpg')
TOSText = 'Terms of Service go here.'
if os.path.isfile(baseDir + '/accounts/tos.txt'):
with open(baseDir + '/accounts/tos.txt', 'r') as file:
TOSText = file.read()
TOSForm = ''
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
termsCSS = getCSS(baseDir, cssFilename, cssCache)
if termsCSS:
if httpPrefix != 'https':
termsCSS = termsCSS.replace('https://', httpPrefix+'://')
TOSForm = htmlHeader(cssFilename, termsCSS)
TOSForm += '' + TOSText + '
\n'
if adminNickname:
adminActor = httpPrefix + '://' + domainFull + \
'/users/' + adminNickname
TOSForm += \
'\n'
TOSForm += htmlFooter()
return TOSForm
def htmlAbout(cssCache: {}, baseDir: str, httpPrefix: str,
domainFull: str, onionDomain: str) -> str:
"""Show the about screen
"""
adminNickname = getConfigParam(baseDir, 'admin')
if not os.path.isfile(baseDir + '/accounts/about.txt'):
copyfile(baseDir + '/default_about.txt',
baseDir + '/accounts/about.txt')
if os.path.isfile(baseDir + '/accounts/login-background-custom.jpg'):
if not os.path.isfile(baseDir + '/accounts/login-background.jpg'):
copyfile(baseDir + '/accounts/login-background-custom.jpg',
baseDir + '/accounts/login-background.jpg')
aboutText = 'Information about this instance goes here.'
if os.path.isfile(baseDir + '/accounts/about.txt'):
with open(baseDir + '/accounts/about.txt', 'r') as aboutFile:
aboutText = aboutFile.read()
aboutForm = ''
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
aboutCSS = getCSS(baseDir, cssFilename, cssCache)
if aboutCSS:
if httpPrefix != 'http':
aboutCSS = aboutCSS.replace('https://',
httpPrefix + '://')
aboutForm = htmlHeader(cssFilename, aboutCSS)
aboutForm += '' + aboutText + '
'
if onionDomain:
aboutForm += \
'\n' + \
'' + \
'http://' + onionDomain + '
\n \n'
if adminNickname:
adminActor = '/users/' + adminNickname
aboutForm += \
'\n'
aboutForm += htmlFooter()
return aboutForm
def htmlHashtagBlocked(cssCache: {}, baseDir: str, translate: {}) -> str:
"""Show the screen for a blocked hashtag
"""
blockedHashtagForm = ''
cssFilename = baseDir + '/epicyon-suspended.css'
if os.path.isfile(baseDir + '/suspended.css'):
cssFilename = baseDir + '/suspended.css'
blockedHashtagCSS = getCSS(baseDir, cssFilename, cssCache)
if blockedHashtagCSS:
blockedHashtagForm = htmlHeader(cssFilename, blockedHashtagCSS)
blockedHashtagForm += '\n'
blockedHashtagForm += htmlFooter()
return blockedHashtagForm
def htmlSuspended(cssCache: {}, baseDir: str) -> str:
"""Show the screen for suspended accounts
"""
suspendedForm = ''
cssFilename = baseDir + '/epicyon-suspended.css'
if os.path.isfile(baseDir + '/suspended.css'):
cssFilename = baseDir + '/suspended.css'
suspendedCSS = getCSS(baseDir, cssFilename, cssCache)
if suspendedCSS:
suspendedForm = htmlHeader(cssFilename, suspendedCSS)
suspendedForm += '\n'
suspendedForm += ' Account Suspended
\n'
suspendedForm += ' See Terms of Service
\n'
suspendedForm += ' \n'
suspendedForm += htmlFooter()
return suspendedForm
def htmlNewPostDropDown(scopeIcon: str, scopeDescription: str,
replyStr: str,
translate: {},
iconsDir: str,
showPublicOnDropdown: bool,
defaultTimeline: str,
pathBase: str,
dropdownNewPostSuffix: str,
dropdownNewBlogSuffix: str,
dropdownUnlistedSuffix: str,
dropdownFollowersSuffix: str,
dropdownDMSuffix: str,
dropdownReminderSuffix: str,
dropdownEventSuffix: str,
dropdownReportSuffix: str) -> str:
"""Returns the html for a drop down list of new post types
"""
dropDownContent = '\n'
dropDownContent += '
\n'
dropDownContent += '
\n'
dropDownContent += ' ' + \
scopeDescription + ' \n'
dropDownContent += '
\n'
dropDownContent += '
\n'
return dropDownContent
def htmlNewPost(cssCache: {}, mediaInstance: bool, translate: {},
baseDir: str, httpPrefix: str,
path: str, inReplyTo: str,
mentions: [],
reportUrl: str, pageNumber: int,
nickname: str, domain: str,
domainFull: str,
defaultTimeline: str, newswire: {}) -> str:
"""New post screen
"""
iconsDir = getIconsDir(baseDir)
replyStr = ''
showPublicOnDropdown = True
messageBoxHeight = 400
# filename of the banner shown at the top
bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain)
if not path.endswith('/newshare'):
if not path.endswith('/newreport'):
if not inReplyTo or path.endswith('/newreminder'):
newPostText = '' + \
translate['Write your post text below.'] + '
\n'
else:
newPostText = \
'' + \
translate['Write your reply to'] + \
' ' + \
translate['this post'] + '
\n'
replyStr = ' \n'
# if replying to a non-public post then also make
# this post non-public
if not isPublicPostFromUrl(baseDir, nickname, domain,
inReplyTo):
newPostPath = path
if '?' in newPostPath:
newPostPath = newPostPath.split('?')[0]
if newPostPath.endswith('/newpost'):
path = path.replace('/newpost', '/newfollowers')
elif newPostPath.endswith('/newunlisted'):
path = path.replace('/newunlisted', '/newfollowers')
showPublicOnDropdown = False
else:
newPostText = \
'' + \
translate['Write your report below.'] + '
\n'
# custom report header with any additional instructions
if os.path.isfile(baseDir + '/accounts/report.txt'):
with open(baseDir + '/accounts/report.txt', 'r') as file:
customReportText = file.read()
if '' not in customReportText:
customReportText = \
'' + \
customReportText + '
\n'
repStr = ''
customReportText = \
customReportText.replace('
', repStr)
newPostText += customReportText
idx = 'This message only goes to moderators, even if it ' + \
'mentions other fediverse addresses.'
newPostText += \
'
' + translate[idx] + '
\n' + \
'' + translate['Also see'] + \
' ' + \
translate['Terms of Service'] + '
\n'
else:
newPostText = \
'' + \
translate['Enter the details for your shared item below.'] + \
'
\n'
if path.endswith('/newquestion'):
newPostText = \
'' + \
translate['Enter the choices for your question below.'] + \
'
\n'
if os.path.isfile(baseDir + '/accounts/newpost.txt'):
with open(baseDir + '/accounts/newpost.txt', 'r') as file:
newPostText = \
'' + file.read() + '
\n'
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
newPostCSS = getCSS(baseDir, cssFilename, cssCache)
if newPostCSS:
if httpPrefix != 'https':
newPostCSS = newPostCSS.replace('https://',
httpPrefix + '://')
if '?' in path:
path = path.split('?')[0]
pathBase = path.replace('/newreport', '').replace('/newpost', '')
pathBase = pathBase.replace('/newblog', '').replace('/newshare', '')
pathBase = pathBase.replace('/newunlisted', '')
pathBase = pathBase.replace('/newevent', '')
pathBase = pathBase.replace('/newreminder', '')
pathBase = pathBase.replace('/newfollowers', '').replace('/newdm', '')
newPostImageSection = ' '
if not path.endswith('/newevent'):
newPostImageSection += \
' ' + \
translate['Image description'] + ' \n'
else:
newPostImageSection += \
' ' + \
translate['Event banner image description'] + ' \n'
newPostImageSection += \
' \n'
if path.endswith('/newevent'):
newPostImageSection += \
' ' + \
translate['Banner image'] + ' \n'
newPostImageSection += \
' \n'
else:
newPostImageSection += \
' \n'
newPostImageSection += '
\n'
scopeIcon = 'scope_public.png'
scopeDescription = translate['Public']
placeholderSubject = \
translate['Subject or Content Warning (optional)'] + '...'
placeholderMentions = ''
if inReplyTo:
# mentionsAndContent = getMentionsString(content)
placeholderMentions = \
translate['Replying to'] + '...'
placeholderMessage = translate['Write something'] + '...'
extraFields = ''
endpoint = 'newpost'
if path.endswith('/newblog'):
placeholderSubject = translate['Title']
scopeIcon = 'scope_blog.png'
if defaultTimeline != 'tlnews':
scopeDescription = translate['Blog']
else:
scopeDescription = translate['Article']
endpoint = 'newblog'
elif path.endswith('/newunlisted'):
scopeIcon = 'scope_unlisted.png'
scopeDescription = translate['Unlisted']
endpoint = 'newunlisted'
elif path.endswith('/newfollowers'):
scopeIcon = 'scope_followers.png'
scopeDescription = translate['Followers']
endpoint = 'newfollowers'
elif path.endswith('/newdm'):
scopeIcon = 'scope_dm.png'
scopeDescription = translate['DM']
endpoint = 'newdm'
elif path.endswith('/newreminder'):
scopeIcon = 'scope_reminder.png'
scopeDescription = translate['Reminder']
endpoint = 'newreminder'
elif path.endswith('/newevent'):
scopeIcon = 'scope_event.png'
scopeDescription = translate['Event']
endpoint = 'newevent'
placeholderSubject = translate['Event name']
placeholderMessage = translate['Describe the event'] + '...'
elif path.endswith('/newreport'):
scopeIcon = 'scope_report.png'
scopeDescription = translate['Report']
endpoint = 'newreport'
elif path.endswith('/newquestion'):
scopeIcon = 'scope_question.png'
scopeDescription = translate['Question']
placeholderMessage = translate['Enter your question'] + '...'
endpoint = 'newquestion'
extraFields = '\n'
extraFields += ' ' + \
translate['Possible answers'] + ': \n'
for questionCtr in range(8):
extraFields += \
' \n'
extraFields += \
' ' + \
translate['Duration of listing in days'] + \
': \n'
extraFields += '
'
elif path.endswith('/newshare'):
scopeIcon = 'scope_share.png'
scopeDescription = translate['Shared Item']
placeholderSubject = translate['Name of the shared item'] + '...'
placeholderMessage = \
translate['Description of the item being shared'] + '...'
endpoint = 'newshare'
extraFields = '\n'
extraFields += \
' ' + \
translate['Type of shared item. eg. hat'] + ': \n'
extraFields += \
' \n'
extraFields += \
' ' + \
translate['Category of shared item. eg. clothing'] + ': \n'
extraFields += \
' \n'
extraFields += \
' ' + \
translate['Duration of listing in days'] + ': \n'
extraFields += ' \n'
extraFields += '
\n'
extraFields += '\n'
extraFields += \
'' + \
translate['City or location of the shared item'] + ': \n'
extraFields += ' \n'
extraFields += '
\n'
citationsStr = ''
if endpoint == 'newblog':
citationsFilename = \
baseDir + '/accounts/' + \
nickname + '@' + domain + '/.citations.txt'
if os.path.isfile(citationsFilename):
citationsStr = '\n'
citationsStr += '
' + \
translate['Citations'] + ':
\n'
citationsStr += '
\n'
citationsSeparator = '#####'
with open(citationsFilename, "r") as f:
citations = f.readlines()
for line in citations:
if citationsSeparator not in line:
continue
sections = line.strip().split(citationsSeparator)
if len(sections) != 3:
continue
title = sections[1]
link = sections[2]
citationsStr += \
' ' + \
title + ' '
citationsStr += ' \n'
citationsStr += '
\n'
dateAndLocation = ''
if endpoint != 'newshare' and \
endpoint != 'newreport' and \
endpoint != 'newquestion':
dateAndLocation = '\n'
if endpoint == 'newevent':
# event status
dateAndLocation += '' + \
translate['Status of the event'] + ': \n'
dateAndLocation += ' \n'
dateAndLocation += '' + \
translate['Tentative'] + ' \n'
dateAndLocation += ' \n'
dateAndLocation += '' + \
translate['Confirmed'] + ' \n'
dateAndLocation += ' \n'
dateAndLocation += '' + \
translate['Cancelled'] + ' \n'
dateAndLocation += '
\n'
dateAndLocation += '\n'
# maximum attendees
dateAndLocation += '' + \
translate['Maximum attendees'] + ': \n'
dateAndLocation += ' \n'
dateAndLocation += '
\n'
dateAndLocation += '\n'
# event joining options
dateAndLocation += '' + \
translate['Joining'] + ': \n'
dateAndLocation += ' \n'
dateAndLocation += '' + \
translate['Anyone can join'] + ' \n'
dateAndLocation += ' \n'
dateAndLocation += '' + \
translate['Apply to join'] + ' \n'
dateAndLocation += ' \n'
dateAndLocation += '' + \
translate['Invitation only'] + ' \n'
dateAndLocation += '
\n'
dateAndLocation += '\n'
dateAndLocation += '\n'
dateAndLocation += '\n'
dateAndLocation += '' + \
translate['Moderation policy or code of conduct'] + \
': \n'
dateAndLocation += \
' \n'
dateAndLocation += '
\n'
dateAndLocation += '\n'
dateAndLocation += '' + \
translate['Location'] + ': \n'
dateAndLocation += ' \n'
if endpoint == 'newevent':
dateAndLocation += '' + \
translate['Ticket URL'] + ': \n'
dateAndLocation += ' \n'
dateAndLocation += '' + \
translate['Categories'] + ': \n'
dateAndLocation += ' \n'
dateAndLocation += '
\n'
newPostForm = htmlHeader(cssFilename, newPostCSS)
newPostForm += \
'\n'
newPostForm += ' \n'
mentionsStr = ''
for m in mentions:
mentionNickname = getNicknameFromActor(m)
if not mentionNickname:
continue
mentionDomain, mentionPort = getDomainFromActor(m)
if not mentionDomain:
continue
if mentionPort:
mentionsHandle = \
'@' + mentionNickname + '@' + \
mentionDomain + ':' + str(mentionPort)
else:
mentionsHandle = '@' + mentionNickname + '@' + mentionDomain
if mentionsHandle not in mentionsStr:
mentionsStr += mentionsHandle + ' '
# build suffixes so that any replies or mentions are
# preserved when switching between scopes
dropdownNewPostSuffix = '/newpost'
dropdownNewBlogSuffix = '/newblog'
dropdownUnlistedSuffix = '/newunlisted'
dropdownFollowersSuffix = '/newfollowers'
dropdownDMSuffix = '/newdm'
dropdownEventSuffix = '/newevent'
dropdownReminderSuffix = '/newreminder'
dropdownReportSuffix = '/newreport'
if inReplyTo or mentions:
dropdownNewPostSuffix = ''
dropdownNewBlogSuffix = ''
dropdownUnlistedSuffix = ''
dropdownFollowersSuffix = ''
dropdownDMSuffix = ''
dropdownEventSuffix = ''
dropdownReminderSuffix = ''
dropdownReportSuffix = ''
if inReplyTo:
dropdownNewPostSuffix += '?replyto=' + inReplyTo
dropdownNewBlogSuffix += '?replyto=' + inReplyTo
dropdownUnlistedSuffix += '?replyto=' + inReplyTo
dropdownFollowersSuffix += '?replyfollowers=' + inReplyTo
dropdownDMSuffix += '?replydm=' + inReplyTo
for mentionedActor in mentions:
dropdownNewPostSuffix += '?mention=' + mentionedActor
dropdownNewBlogSuffix += '?mention=' + mentionedActor
dropdownUnlistedSuffix += '?mention=' + mentionedActor
dropdownFollowersSuffix += '?mention=' + mentionedActor
dropdownDMSuffix += '?mention=' + mentionedActor
dropdownReportSuffix += '?mention=' + mentionedActor
dropDownContent = ''
if not reportUrl:
dropDownContent = \
htmlNewPostDropDown(scopeIcon, scopeDescription,
replyStr,
translate,
iconsDir,
showPublicOnDropdown,
defaultTimeline,
pathBase,
dropdownNewPostSuffix,
dropdownNewBlogSuffix,
dropdownUnlistedSuffix,
dropdownFollowersSuffix,
dropdownDMSuffix,
dropdownReminderSuffix,
dropdownEventSuffix,
dropdownReportSuffix)
else:
mentionsStr = 'Re: ' + reportUrl + '\n\n' + mentionsStr
newPostForm += \
'\n'
newPostForm += ' \n'
newPostForm += \
'
' + newPostText + ' \n'
newPostForm += '
\n'
newPostForm += '
\n'
newPostForm += '' + dropDownContent + ' \n'
newPostForm += \
' \n'
newPostForm += ' \n'
newPostForm += '
\n'
newPostForm += '
\n'
newPostForm += '
\n'
newPostForm += replyStr
if mediaInstance and not replyStr:
newPostForm += newPostImageSection
newPostForm += \
'
' + placeholderSubject + ' '
newPostForm += '
'
newPostForm += ''
selectedStr = ' selected'
if inReplyTo or endpoint == 'newdm':
if inReplyTo:
newPostForm += \
'
' + placeholderMentions + \
' \n'
else:
newPostForm += \
'
' \
'' + \
translate['Send to'] + ':' + ' 📄 \n'
newPostForm += \
'
\n'
newPostForm += \
htmlFollowingDataList(baseDir, nickname, domain, domainFull)
newPostForm += ''
selectedStr = ''
newPostForm += \
'
' + placeholderMessage + ' '
if mediaInstance:
messageBoxHeight = 200
if endpoint == 'newquestion':
messageBoxHeight = 100
elif endpoint == 'newblog':
messageBoxHeight = 800
newPostForm += \
'
\n'
newPostForm += extraFields + citationsStr + dateAndLocation
if not mediaInstance or replyStr:
newPostForm += newPostImageSection
newPostForm += '
\n'
newPostForm += ' \n'
if not reportUrl:
newPostForm = \
newPostForm.replace('', '')
newPostForm += htmlFooter()
return newPostForm
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 htmlIndividualShare(actor: str, item: {}, translate: {},
showContact: bool, removeButton: bool) -> str:
"""Returns an individual shared item as html
"""
profileStr = '\n'
profileStr += '
' + item['displayName'] + '
\n'
if item.get('imageUrl'):
profileStr += '
\n'
profileStr += \
' \n \n'
profileStr += '
' + item['summary'] + '
\n'
profileStr += \
'
' + translate['Type'] + ': ' + item['itemType'] + ' '
profileStr += \
'' + translate['Category'] + ': ' + item['category'] + ' '
profileStr += \
'' + translate['Location'] + ': ' + item['location'] + '
\n'
if showContact:
contactActor = item['actor']
profileStr += \
'
' + \
translate['Contact'] + ' \n'
if removeButton:
profileStr += \
' ' + \
translate['Remove'] + ' \n'
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 htmlSharesTimeline(translate: {}, pageNumber: int, itemsPerPage: int,
baseDir: str, actor: str,
nickname: str, domain: str, port: int,
maxSharesPerAccount: int, httpPrefix: str) -> str:
"""Show shared items timeline as html
"""
sharesJson, lastPage = \
sharesTimelineJson(actor, pageNumber, itemsPerPage,
baseDir, maxSharesPerAccount)
domainFull = domain
if port != 80 and port != 443:
if ':' not in domain:
domainFull = domain + ':' + str(port)
actor = httpPrefix + '://' + domainFull + '/users/' + nickname
timelineStr = ''
if pageNumber > 1:
iconsDir = getIconsDir(baseDir)
timelineStr += \
' \n' + \
' \n' + \
' \n'
separatorStr = htmlPostSeparator(baseDir, None)
for published, item in sharesJson.items():
showContactButton = False
if item['actor'] != actor:
showContactButton = True
showRemoveButton = False
if item['actor'] == actor:
showRemoveButton = True
timelineStr += separatorStr + \
htmlIndividualShare(actor, item, translate,
showContactButton, showRemoveButton)
if not lastPage:
iconsDir = getIconsDir(baseDir)
timelineStr += \
' \n' + \
' \n' + \
' \n'
return timelineStr
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 += \
' ' + translate['Donate'] + \
'
\n'
if emailAddress:
donateSection += \
'' + translate['Email'] + ': ' + emailAddress + '
\n'
if xmppAddress:
donateSection += \
'' + translate['XMPP'] + ': '+xmppAddress + '
\n'
if matrixAddress:
donateSection += \
'' + translate['Matrix'] + ': ' + matrixAddress + '
\n'
if ssbAddress:
donateSection += \
'SSB: ' + \
ssbAddress + '
\n'
if toxAddress:
donateSection += \
'Tox: ' + \
toxAddress + '
\n'
if 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 = \
'' + \
translate['Switch to timeline view'] + ' '
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 individualFollowAsHtml(translate: {},
baseDir: str, session, wfRequest: {},
personCache: {}, domain: str,
followUrl: str,
authorized: bool,
actorNickname: str,
httpPrefix: str,
projectVersion: str,
buttons=[]) -> str:
"""An individual follow entry on the profile screen
"""
nickname = getNicknameFromActor(followUrl)
domain, port = getDomainFromActor(followUrl)
titleStr = '@' + nickname + '@' + domain
avatarUrl = getPersonAvatarUrl(baseDir, followUrl, personCache, True)
if not avatarUrl:
avatarUrl = followUrl + '/avatar.png'
if domain not in followUrl:
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
avatarUrl2, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, projectVersion,
httpPrefix, nickname,
domain, 'outbox')
if avatarUrl2:
avatarUrl = avatarUrl2
if displayName:
titleStr = displayName + ' ' + titleStr
buttonsStr = ''
if authorized:
for b in buttons:
if b == 'block':
buttonsStr += \
'' + \
translate['Block'] + ' \n'
if b == 'unfollow':
buttonsStr += \
'' + \
translate['Unfollow'] + ' \n'
resultStr = '\n'
return resultStr
def htmlHighlightLabel(label: str, highlight: bool) -> str:
"""If the give text should be highlighted then return
the appropriate markup.
This is so that in shell browsers, like lynx, it's possible
to see if the replies or DM button are highlighted.
"""
if not highlight:
return label
return '*' + str(label) + '*'
def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str,
httpPrefix: str, translate: {},
iconsDir: str, editor: bool,
showBackButton: bool, timelinePath: str,
rssIconAtTop: bool, showHeaderImage: bool,
frontPage: bool) -> str:
"""Returns html content for the left column
"""
htmlStr = ''
separatorStr = htmlPostSeparator(baseDir, 'left')
domain = domainFull
if ':' in domain:
domain = domain.split(':')
editImageClass = ''
if showHeaderImage:
leftImageFile, leftColumnImageFilename = \
getLeftImageFile(baseDir, nickname, domain)
if not os.path.isfile(leftColumnImageFilename):
theme = getConfigParam(baseDir, 'theme').lower()
if theme == 'default':
theme = ''
else:
theme = '_' + theme
themeLeftImageFile, themeLeftColumnImageFilename = \
getImageFile(baseDir, 'left_col_image', baseDir + '/img',
nickname, domain)
if os.path.isfile(themeLeftColumnImageFilename):
leftColumnImageFilename = \
baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + themeLeftImageFile
copyfile(themeLeftColumnImageFilename,
leftColumnImageFilename)
leftImageFile = themeLeftImageFile
# show the image at the top of the column
editImageClass = 'leftColEdit'
if os.path.isfile(leftColumnImageFilename):
editImageClass = 'leftColEditImage'
htmlStr += \
'\n \n' + \
' \n' + \
' \n'
if showBackButton:
htmlStr += \
' ' + \
'
' + \
'' + \
translate['Go Back'] + ' \n'
if (editor or rssIconAtTop) and not showHeaderImage:
htmlStr += '
'
if editImageClass == 'leftColEdit':
htmlStr += '\n
\n'
htmlStr += ' \n'
if editor:
# show the edit icon
htmlStr += \
'
' + \
' \n'
# RSS icon
if nickname != 'news':
# rss feed for this account
rssUrl = httpPrefix + '://' + domainFull + \
'/blog/' + nickname + '/rss.xml'
else:
# rss feed for all accounts on the instance
rssUrl = httpPrefix + '://' + domainFull + '/blog/rss.xml'
if not frontPage:
rssTitle = translate['RSS feed for your blog']
else:
rssTitle = translate['RSS feed for this site']
rssIconStr = \
'
' + \
' \n'
if rssIconAtTop:
htmlStr += rssIconStr
htmlStr += '
\n'
if editImageClass == 'leftColEdit':
htmlStr += ' \n'
if (editor or rssIconAtTop) and not showHeaderImage:
htmlStr += '
'
# if showHeaderImage:
# htmlStr += '
'
linksFilename = baseDir + '/accounts/links.txt'
linksFileContainsEntries = False
if os.path.isfile(linksFilename):
linksList = None
with open(linksFilename, "r") as f:
linksList = f.readlines()
if linksList:
for lineStr in linksList:
if ' ' not in lineStr:
if '#' not in lineStr:
if '*' not in lineStr:
continue
lineStr = lineStr.strip()
words = lineStr.split(' ')
# get the link
linkStr = None
for word in words:
if word == '#':
continue
if word == '*':
continue
if '://' in word:
linkStr = word
break
if linkStr:
lineStr = lineStr.replace(linkStr, '').strip()
# avoid any dubious scripts being added
if '<' not in lineStr:
# remove trailing comma if present
if lineStr.endswith(','):
lineStr = lineStr[:len(lineStr)-1]
# add link to the returned html
htmlStr += \
'
' + \
lineStr + '
\n'
linksFileContainsEntries = True
else:
if lineStr.startswith('#') or lineStr.startswith('*'):
lineStr = lineStr[1:].strip()
htmlStr += separatorStr
htmlStr += \
' \n'
else:
htmlStr += \
'
' + lineStr + '
\n'
linksFileContainsEntries = True
if linksFileContainsEntries and not rssIconAtTop:
htmlStr += '
' + rssIconStr + '
'
return htmlStr
def votesIndicator(totalVotes: int, positiveVoting: bool) -> str:
"""Returns an indicator of the number of votes on a newswire item
"""
if totalVotes <= 0:
return ''
totalVotesStr = ' '
for v in range(totalVotes):
if positiveVoting:
totalVotesStr += '✓'
else:
totalVotesStr += '✗'
return totalVotesStr
def htmlNewswire(baseDir: str, newswire: {}, nickname: str, moderator: bool,
translate: {}, positiveVoting: bool, iconsDir: str) -> str:
"""Converts a newswire dict into html
"""
separatorStr = htmlPostSeparator(baseDir, 'right')
htmlStr = ''
for dateStr, item in newswire.items():
publishedDate = \
datetime.strptime(dateStr, "%Y-%m-%d %H:%M:%S%z")
dateShown = publishedDate.strftime("%Y-%m-%d %H:%M")
dateStrLink = dateStr.replace('T', ' ')
dateStrLink = dateStrLink.replace('Z', '')
moderatedItem = item[5]
htmlStr += separatorStr
if moderatedItem and 'vote:' + nickname in item[2]:
totalVotesStr = ''
totalVotes = 0
if moderator:
totalVotes = votesOnNewswireItem(item[2])
totalVotesStr = \
votesIndicator(totalVotes, positiveVoting)
title = removeLongWords(item[0], 16, []).replace('\n', '
')
htmlStr += '
' + \
'' + \
'' + title + \
' ' + totalVotesStr
if moderator:
htmlStr += \
' ' + dateShown + ''
htmlStr += '
\n'
else:
htmlStr += '
'
htmlStr += dateShown + ' \n'
else:
totalVotesStr = ''
totalVotes = 0
if moderator:
if moderatedItem:
totalVotes = votesOnNewswireItem(item[2])
# show a number of ticks or crosses for how many
# votes for or against
totalVotesStr = \
votesIndicator(totalVotes, positiveVoting)
title = removeLongWords(item[0], 16, []).replace('\n', '
')
if moderator and moderatedItem:
htmlStr += '
' + \
'' + \
title + ' ' + totalVotesStr
htmlStr += ' ' + dateShown
htmlStr += ''
htmlStr += ' '
htmlStr += '
\n'
else:
htmlStr += '
' + \
'' + \
title + ' ' + \
totalVotesStr
htmlStr += ' '
htmlStr += dateShown + '
\n'
return htmlStr
def htmlCitations(baseDir: str, nickname: str, domain: str,
httpPrefix: str, defaultTimeline: str,
translate: {}, newswire: {}, cssCache: {},
blogTitle: str, blogContent: str,
blogImageFilename: str,
blogImageAttachmentMediaType: str,
blogImageDescription: str) -> str:
"""Show the citations screen when creating a blog
"""
htmlStr = ''
# create a list of dates for citations
# these can then be used to re-select checkboxes later
citationsFilename = \
baseDir + '/accounts/' + \
nickname + '@' + domain + '/.citations.txt'
citationsSelected = []
if os.path.isfile(citationsFilename):
citationsSeparator = '#####'
with open(citationsFilename, "r") as f:
citations = f.readlines()
for line in citations:
if citationsSeparator not in line:
continue
sections = line.strip().split(citationsSeparator)
if len(sections) != 3:
continue
dateStr = sections[0]
citationsSelected.append(dateStr)
# the css filename
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
profileStyle = getCSS(baseDir, cssFilename, cssCache)
if profileStyle:
# replace any https within the css with whatever prefix is needed
if httpPrefix != 'https':
profileStyle = \
profileStyle.replace('https://', httpPrefix + '://')
# iconsDir = getIconsDir(baseDir)
htmlStr = htmlHeader(cssFilename, profileStyle)
# top banner
bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain)
htmlStr += \
'
\n'
htmlStr += ' \n'
htmlStr += \
'
\n'
htmlStr += ' \n'
htmlStr += translate['Choose newswire items ' +
'referenced in your article'] + ' '
if blogTitle is None:
blogTitle = ''
htmlStr += \
' \n'
if blogContent is None:
blogContent = ''
htmlStr += \
' \n'
# submit button
htmlStr += \
' \n'
htmlStr += ' \n'
citationsSeparator = '#####'
# list of newswire items
if newswire:
ctr = 0
for dateStr, item in newswire.items():
# should this checkbox be selected?
selectedStr = ''
if dateStr in citationsSelected:
selectedStr = ' checked'
publishedDate = \
datetime.strptime(dateStr, "%Y-%m-%d %H:%M:%S%z")
dateShown = publishedDate.strftime("%Y-%m-%d %H:%M")
title = removeLongWords(item[0], 16, []).replace('\n', ' ')
link = item[1]
citationValue = \
dateStr + citationsSeparator + \
title + citationsSeparator + \
link
htmlStr += \
' ' + \
'' + title + ' '
htmlStr += '' + \
dateShown + ' \n'
ctr += 1
htmlStr += ' \n'
return htmlStr + htmlFooter()
def getRightColumnContent(baseDir: str, nickname: str, domainFull: str,
httpPrefix: str, translate: {},
iconsDir: str, moderator: bool, editor: bool,
newswire: {}, positiveVoting: bool,
showBackButton: bool, timelinePath: str,
showPublishButton: bool,
showPublishAsIcon: bool,
rssIconAtTop: bool,
publishButtonAtTop: bool,
authorized: bool,
showHeaderImage: bool) -> str:
"""Returns html content for the right column
"""
htmlStr = ''
domain = domainFull
if ':' in domain:
domain = domain.split(':')
if authorized:
# only show the publish button if logged in, otherwise replace it with
# a login button
publishButtonStr = \
'
' + \
'' + \
translate['Publish'] + ' \n'
else:
# if not logged in then replace the publish button with
# a login button
publishButtonStr = \
'
' + \
translate['Login'] + ' \n'
# show publish button at the top if needed
if publishButtonAtTop:
htmlStr += '
' + publishButtonStr + ' '
# show a column header image, eg. title of the theme or newswire banner
editImageClass = ''
if showHeaderImage:
rightImageFile, rightColumnImageFilename = \
getRightImageFile(baseDir, nickname, domain)
if not os.path.isfile(rightColumnImageFilename):
theme = getConfigParam(baseDir, 'theme').lower()
if theme == 'default':
theme = ''
else:
theme = '_' + theme
themeRightImageFile, themeRightColumnImageFilename = \
getImageFile(baseDir, 'right_col_image', baseDir + '/img',
nickname, domain)
if os.path.isfile(themeRightColumnImageFilename):
rightColumnImageFilename = \
baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + themeRightImageFile
copyfile(themeRightColumnImageFilename,
rightColumnImageFilename)
rightImageFile = themeRightImageFile
# show the image at the top of the column
editImageClass = 'rightColEdit'
if os.path.isfile(rightColumnImageFilename):
editImageClass = 'rightColEditImage'
htmlStr += \
'\n
\n' + \
' \n' + \
' \n'
if (showPublishButton or editor or rssIconAtTop) and not showHeaderImage:
htmlStr += '
'
if editImageClass == 'rightColEdit':
htmlStr += '\n
\n'
# whether to show a back icon
# This is probably going to be osolete soon
if showBackButton:
htmlStr += \
' ' + \
'' + \
translate['Go Back'] + ' \n'
if showPublishButton and not publishButtonAtTop:
if not showPublishAsIcon:
htmlStr += publishButtonStr
# show the edit icon
if editor:
if os.path.isfile(baseDir + '/accounts/newswiremoderation.txt'):
# show the edit icon highlighted
htmlStr += \
' ' + \
' \n'
else:
# show the edit icon
htmlStr += \
' ' + \
' \n'
# show the RSS icon
rssIconStr = \
' ' + \
' \n'
if rssIconAtTop:
htmlStr += rssIconStr
# show publish icon at top
if showPublishButton:
if showPublishAsIcon:
htmlStr += \
' ' + \
' \n'
if editImageClass == 'rightColEdit':
htmlStr += ' \n'
else:
if showHeaderImage:
htmlStr += '
\n'
if (showPublishButton or editor or rssIconAtTop) and not showHeaderImage:
htmlStr += '
'
# show the newswire lines
newswireContentStr = \
htmlNewswire(baseDir, newswire, nickname, moderator, translate,
positiveVoting, iconsDir)
htmlStr += newswireContentStr
# show the rss icon at the bottom, typically on the right hand side
if newswireContentStr and not rssIconAtTop:
htmlStr += '
' + rssIconStr + '
'
return htmlStr
def htmlLinksMobile(cssCache: {}, baseDir: str,
nickname: str, domainFull: str,
httpPrefix: str, translate,
timelinePath: str, authorized: bool,
rssIconAtTop: bool,
iconsAsButtons: bool,
defaultTimeline: str) -> str:
"""Show the left column links within mobile view
"""
htmlStr = ''
# the css filename
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
profileStyle = getCSS(baseDir, cssFilename, cssCache)
if profileStyle:
# replace any https within the css with whatever prefix is needed
if httpPrefix != 'https':
profileStyle = \
profileStyle.replace('https://', httpPrefix + '://')
iconsDir = getIconsDir(baseDir)
# is the user a site editor?
if nickname == 'news':
editor = False
else:
editor = isEditor(baseDir, nickname)
domain = domainFull
if ':' in domain:
domain = domain.split(':')[0]
htmlStr = htmlHeader(cssFilename, profileStyle)
bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain)
htmlStr += \
'
' + \
' \n'
htmlStr += '
' + \
headerButtonsFrontScreen(translate, nickname,
'links', authorized,
iconsAsButtons, iconsDir) + ' '
htmlStr += \
getLeftColumnContent(baseDir, nickname, domainFull,
httpPrefix, translate,
iconsDir, editor,
False, timelinePath,
rssIconAtTop, False, False)
htmlStr += '
\n' + htmlFooter()
return htmlStr
def htmlNewswireMobile(cssCache: {}, baseDir: str, nickname: str,
domain: str, domainFull: str,
httpPrefix: str, translate: {},
newswire: {},
positiveVoting: bool,
timelinePath: str,
showPublishAsIcon: bool,
authorized: bool,
rssIconAtTop: bool,
iconsAsButtons: bool,
defaultTimeline: str) -> str:
"""Shows the mobile version of the newswire right column
"""
htmlStr = ''
# the css filename
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
profileStyle = getCSS(baseDir, cssFilename, cssCache)
if profileStyle:
# replace any https within the css with whatever prefix is needed
if httpPrefix != 'https':
profileStyle = \
profileStyle.replace('https://',
httpPrefix + '://')
iconsDir = getIconsDir(baseDir)
if nickname == 'news':
editor = False
moderator = False
else:
# is the user a moderator?
moderator = isModerator(baseDir, nickname)
# is the user a site editor?
editor = isEditor(baseDir, nickname)
showPublishButton = editor
htmlStr = htmlHeader(cssFilename, profileStyle)
bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain)
htmlStr += \
'' + \
' \n'
htmlStr += '' + \
headerButtonsFrontScreen(translate, nickname,
'newswire', authorized,
iconsAsButtons, iconsDir) + ' '
htmlStr += \
getRightColumnContent(baseDir, nickname, domainFull,
httpPrefix, translate,
iconsDir, moderator, editor,
newswire, positiveVoting,
False, timelinePath, showPublishButton,
showPublishAsIcon, rssIconAtTop, False,
authorized, False)
htmlStr += htmlFooter()
return htmlStr
def headerButtonsFrontScreen(translate: {},
nickname: str, boxName: str,
authorized: bool,
iconsAsButtons: bool,
iconsDir: bool) -> str:
"""Returns the header buttons for the front page of a news instance
"""
headerStr = ''
if nickname == 'news':
buttonFeatures = 'buttonMobile'
buttonNewswire = 'buttonMobile'
buttonLinks = 'buttonMobile'
if boxName == 'features':
buttonFeatures = 'buttonselected'
elif boxName == 'newswire':
buttonNewswire = 'buttonselected'
elif boxName == 'links':
buttonLinks = 'buttonselected'
headerStr += \
' ' + \
'' + \
'' + translate['Features'] + \
' '
if not authorized:
headerStr += \
' ' + \
'' + \
'' + translate['Login'] + \
' '
if iconsAsButtons:
headerStr += \
' ' + \
'' + \
'' + translate['Newswire'] + \
' '
headerStr += \
' ' + \
'' + \
'' + translate['Links'] + \
' '
else:
headerStr += \
' ' + \
' \n'
headerStr += \
' ' + \
' \n'
else:
if not authorized:
headerStr += \
' ' + \
'' + \
'' + translate['Login'] + \
' '
if headerStr:
headerStr = \
'\n \n' + \
headerStr + \
'
\n'
return headerStr
def headerButtonsTimeline(defaultTimeline: str,
boxName: str,
pageNumber: int,
translate: {},
usersPath: str,
mediaButton: str,
blogsButton: str,
newsButton: str,
inboxButton: str,
dmButton: str,
newDM: str,
repliesButton: str,
newReply: str,
minimal: bool,
sentButton: str,
sharesButtonStr: str,
bookmarksButtonStr: str,
eventsButtonStr: str,
moderationButtonStr: str,
newPostButtonStr: str,
baseDir: str,
nickname: str, domain: str,
iconsDir: str,
timelineStartTime,
newCalendarEvent: bool,
calendarPath: str,
calendarImage: str,
followApprovals: str,
iconsAsButtons: bool) -> str:
"""Returns the header at the top of the timeline, containing
buttons for inbox, outbox, search, calendar, etc
"""
# start of the button header with inbox, outbox, etc
tlStr = ''
else:
# NOTE: deliberately no \n at end of line
tlStr += \
'' + \
'' + translate['Links'] + \
' '
if newsHeader:
tlStr += \
'' + \
'' + \
'' + translate['Settings'] + ' '
if not newsHeader:
tlStr += followApprovals
# end of the button header with inbox, outbox, etc
tlStr += '