\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 += \
' '
editNewswireForm += \
'
'
editNewswireForm += htmlFooter()
return editNewswireForm
def htmlEditProfile(translate: {}, baseDir: str, path: str,
domain: str, port: int, httpPrefix: str) -> str:
"""Shows the edit profile screen
"""
imageFormats = '.png, .jpg, .jpeg, .gif, .webp, .avif'
pathOriginal = path
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 ''
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'
with open(cssFilename, 'r') as cssFile:
editProfileCSS = cssFile.read()
if httpPrefix != 'https':
editProfileCSS = \
editProfileCSS.replace('https://', httpPrefix + '://')
instanceStr = ''
moderatorsStr = ''
themesDropdown = ''
adminNickname = getConfigParam(baseDir, 'admin')
if path.startswith('/users/' + adminNickname + '/'):
instanceDescription = \
getConfigParam(baseDir, 'instanceDescription')
instanceDescriptionShort = \
getConfigParam(baseDir, 'instanceDescriptionShort')
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
instanceStr = '
'
instanceStr += \
' ' + \
translate['Instance Title'] + ' '
if instanceTitle:
instanceStr += \
' '
else:
instanceStr += \
' '
instanceStr += \
' ' + \
translate['Instance Short Description'] + ' '
if instanceDescriptionShort:
instanceStr += \
' '
else:
instanceStr += \
' '
instanceStr += \
' ' + \
translate['Instance Description'] + ' '
if instanceDescription:
instanceStr += \
' '
else:
instanceStr += \
' '
instanceStr += \
' ' + \
translate['Instance Logo'] + ' '
instanceStr += \
' '
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 += '
'
themes = getThemesList()
themesDropdown = '
'
themesDropdown += ' ' + translate['Theme'] + ' '
grayscaleFilename = \
baseDir + '/accounts/.grayscale'
grayscale = ''
if os.path.isfile(grayscaleFilename):
grayscale = 'checked'
themesDropdown += \
' ' + translate['Grayscale'] + ' '
themesDropdown += ' '
for themeName in themes:
themesDropdown += ' ' + \
translate[themeName] + ' '
themesDropdown += ' '
if os.path.isfile(baseDir + '/fonts/custom.woff') or \
os.path.isfile(baseDir + '/fonts/custom.woff2') or \
os.path.isfile(baseDir + '/fonts/custom.otf') or \
os.path.isfile(baseDir + '/fonts/custom.ttf'):
themesDropdown += \
' ' + \
translate['Remove the custom font'] + ' '
themesDropdown += '
'
themeName = getConfigParam(baseDir, 'theme')
themesDropdown = \
themesDropdown.replace('
',
' ')
editProfileForm = htmlHeader(cssFilename, editProfileCSS)
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(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'
with open(cssFilename, 'r') as cssFile:
loginCSS = cssFile.read()
# 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(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'
with open(cssFilename, 'r') as cssFile:
termsCSS = cssFile.read()
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(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'
with open(cssFilename, 'r') as cssFile:
aboutCSS = cssFile.read()
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(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'
with open(cssFilename, 'r') as cssFile:
blockedHashtagCSS = cssFile.read()
blockedHashtagForm = htmlHeader(cssFilename, blockedHashtagCSS)
blockedHashtagForm += '\n'
blockedHashtagForm += htmlFooter()
return blockedHashtagForm
def htmlSuspended(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'
with open(cssFilename, 'r') as cssFile:
suspendedCSS = cssFile.read()
suspendedForm = htmlHeader(cssFilename, suspendedCSS)
suspendedForm += '\n'
suspendedForm += ' Account Suspended
\n'
suspendedForm += ' See Terms of Service
\n'
suspendedForm += ' \n'
suspendedForm += htmlFooter()
return suspendedForm
def htmlNewPost(mediaInstance: bool, translate: {},
baseDir: str, httpPrefix: str,
path: str, inReplyTo: str,
mentions: [],
reportUrl: str, pageNumber: int,
nickname: str, domain: str,
domainFull: str) -> str:
"""New post screen
"""
iconsDir = getIconsDir(baseDir)
replyStr = ''
showPublicOnDropdown = True
messageBoxHeight = 400
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'
with open(cssFilename, 'r') as cssFile:
newPostCSS = cssFile.read()
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'
scopeDescription = translate['Blog']
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'
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)
# only show the share option if this is not a reply
shareOptionOnDropdown = ''
questionOptionOnDropdown = ''
if not replyStr:
shareOptionOnDropdown = \
' ' + translate['Shares'] + \
' ' + translate['Describe a shared item'] + ' \n'
questionOptionOnDropdown = \
' ' + translate['Question'] + \
' ' + translate['Ask a question'] + ' \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 += "\n"
dropDownContent += "
\n"
dropDownContent += " \n"
dropDownContent += " \n'
dropDownContent += ' \n'
dropDownContent += ' \n'
dropDownContent += '
\n'
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+dateAndLocation
if not mediaInstance or replyStr:
newPostForm += newPostImageSection
newPostForm += '
\n'
newPostForm += ' \n'
if not reportUrl:
newPostForm = \
newPostForm.replace('', '')
newPostForm += htmlFooter()
return newPostForm
def getFontFromCss(css: str) -> (str, str):
"""Returns the font name and format
"""
if ' url(' not in css:
return None, None
fontName = css.split(" url(")[1].split(")")[0].replace("'", '')
fontFormat = css.split(" format('")[1].split("')")[0]
return fontName, fontFormat
def htmlHeader(cssFilename: str, css: str, lang='en') -> str:
htmlStr = '\n'
htmlStr += '\n'
htmlStr += ' \n'
htmlStr += ' \n'
fontName, fontFormat = getFontFromCss(css)
if fontName:
htmlStr += ' \n'
htmlStr += ' \n'
htmlStr += ' \n'
htmlStr += ' \n'
htmlStr += ' Epicyon \n'
htmlStr += ' \n'
htmlStr += ' \n'
return htmlStr
def htmlFooter() -> str:
htmlStr = ' \n'
htmlStr += '\n'
return htmlStr
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) -> str:
"""Shows posts on the profile screen
These should only be public posts
"""
iconsDir = getIconsDir(baseDir)
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)
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,
False, False, False, True, False)
if postStr:
profileStr += 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 sharesTimelineJson(actor: str, pageNumber: int, itemsPerPage: int,
baseDir: str, maxSharesPerAccount: int) -> ({}, bool):
"""Get a page on the shared items timeline as json
maxSharesPerAccount helps to avoid one person dominating the timeline
by sharing a large number of things
"""
allSharesJson = {}
for subdir, dirs, files in os.walk(baseDir + '/accounts'):
for handle in dirs:
if '@' in handle:
accountDir = baseDir + '/accounts/' + handle
sharesFilename = accountDir + '/shares.json'
if os.path.isfile(sharesFilename):
sharesJson = loadJson(sharesFilename)
if not sharesJson:
continue
nickname = handle.split('@')[0]
# actor who owns this share
owner = actor.split('/users/')[0] + '/users/' + nickname
ctr = 0
for itemID, item in sharesJson.items():
# assign owner to the item
item['actor'] = owner
allSharesJson[str(item['published'])] = item
ctr += 1
if ctr >= maxSharesPerAccount:
break
# sort the shared items in descending order of publication date
sharesJson = OrderedDict(sorted(allSharesJson.items(), reverse=True))
lastPage = False
startIndex = itemsPerPage*pageNumber
maxIndex = len(sharesJson.items())
if maxIndex < itemsPerPage:
lastPage = True
if startIndex >= maxIndex - itemsPerPage:
lastPage = True
startIndex = maxIndex - itemsPerPage
if startIndex < 0:
startIndex = 0
ctr = 0
resultJson = {}
for published, item in sharesJson.items():
if ctr >= startIndex + itemsPerPage:
break
if ctr < startIndex:
ctr += 1
continue
resultJson[published] = item
ctr += 1
return resultJson, lastPage
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'
for published, item in sharesJson.items():
showContactButton = False
if item['actor'] != actor:
showContactButton = True
showRemoveButton = False
if item['actor'] == actor:
showRemoveButton = True
timelineStr += \
htmlIndividualShare(actor, item, translate,
showContactButton, showRemoveButton)
if not lastPage:
iconsDir = getIconsDir(baseDir)
timelineStr += \
' \n' + \
' \n' + \
' \n'
return timelineStr
def htmlProfile(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, projectVersion: str,
baseDir: str, httpPrefix: str, authorized: bool,
profileJson: {}, selected: str,
session, wfRequest: {}, personCache: {},
YTReplacementDomain: str,
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:
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'
if not authorized:
loginButton = \
'' + \
translate['Login'] + ' '
else:
editProfileStr = \
'' + \
translate['Edit'] + ' '
logoutStr = \
'' + \
translate['Logout'] + ' '
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('
', '')
profileHeaderStr = ''
profileHeaderStr += '
'
profileHeaderStr += \
'
'
profileHeaderStr += '
' + displayName + ' '
iconsDir = getIconsDir(baseDir)
profileHeaderStr += \
'
@' + nickname + '@' + domainFull + ' '
profileHeaderStr += \
'' + \
'
'
profileHeaderStr += '
' + profileDescriptionShort + '
'
profileHeaderStr += loginButton
profileHeaderStr += '
'
profileHeaderStr += '
'
profileStr = \
linkToTimelineStart + profileHeaderStr + \
linkToTimelineEnd + donateSection
profileStr += ''
profileStr += followApprovalsSection
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
with open(cssFilename, 'r') as cssFile:
profileStyle = \
cssFile.read().replace('image.png',
profileJson['image']['url'])
licenseStr = \
'' + \
' '
if selected == 'posts':
profileStr += \
htmlProfilePosts(recentPostsCache, maxRecentPosts,
translate,
baseDir, httpPrefix, authorized,
nickname, domain, port,
session, wfRequest, personCache,
projectVersion,
YTReplacementDomain) + 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
profileStr = \
htmlHeader(cssFilename, profileStyle) + profileStr + 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 addEmbeddedAudio(translate: {}, content: str) -> str:
"""Adds embedded audio for mp3/ogg
"""
if not ('.mp3' in content or '.ogg' in content):
return content
if '', '')
if w.endswith('.'):
w = w[:-1]
if w.endswith('"'):
w = w[:-1]
if w.endswith(';'):
w = w[:-1]
if w.endswith(':'):
w = w[:-1]
if not w.endswith(extension):
continue
if not (w.startswith('http') or w.startswith('dat:') or
w.startswith('hyper:') or w.startswith('i2p:') or
w.startswith('gnunet:') or
'/' in w):
continue
url = w
content += '\n\n'
content += \
''
content += \
translate['Your browser does not support the audio element.']
content += ' \n \n'
return content
def addEmbeddedVideo(translate: {}, content: str,
width=400, height=300) -> str:
"""Adds embedded video for mp4/webm/ogv
"""
if not ('.mp4' in content or '.webm' in content or '.ogv' in content):
return content
if '', '')
if w.endswith('.'):
w = w[:-1]
if w.endswith('"'):
w = w[:-1]
if w.endswith(';'):
w = w[:-1]
if w.endswith(':'):
w = w[:-1]
if not w.endswith(extension):
continue
if not (w.startswith('http') or w.startswith('dat:') or
w.startswith('hyper:') or w.startswith('i2p:') or
w.startswith('gnunet:') or
'/' in w):
continue
url = w
content += \
'\n\n'
content += \
'\n'
content += \
translate['Your browser does not support the video element.']
content += ' \n \n'
return content
def addEmbeddedVideoFromSites(translate: {}, content: str,
width=400, height=300) -> str:
"""Adds embedded videos
"""
if '>vimeo.com/' in content:
url = content.split('>vimeo.com/')[1]
if '<' in url:
url = url.split('<')[0]
content = \
content + "\n\n \n"
return content
videoSite = 'https://www.youtube.com'
if '"' + videoSite in content:
url = content.split('"' + videoSite)[1]
if '"' in url:
url = url.split('"')[0].replace('/watch?v=', '/embed/')
if '&' in url:
url = url.split('&')[0]
content = \
content + "\n\n \n"
return content
invidiousSites = ('https://invidio.us',
'https://invidious.snopyta.org',
'http://c7hqkpkpemu6e7emz5b4vy' +
'z7idjgdvgaaa3dyimmeojqbgpea3xqjoid.onion',
'http://axqzx4s6s54s32yentfqojs3x5i7faxza6xo3ehd4' +
'bzzsg2ii4fv2iid.onion')
for videoSite in invidiousSites:
if '"' + videoSite in content:
url = content.split('"' + videoSite)[1]
if '"' in url:
url = url.split('"')[0].replace('/watch?v=', '/embed/')
if '&' in url:
url = url.split('&')[0]
content = \
content + "\n\n \n"
return content
videoSite = 'https://media.ccc.de'
if '"' + videoSite in content:
url = content.split('"' + videoSite)[1]
if '"' in url:
url = url.split('"')[0]
if not url.endswith('/oembed'):
url = url + '/oembed'
content = \
content + "\n\n \n"
return content
if '"https://' in content:
# A selection of the current larger peertube sites, mostly
# French and German language
# These have been chosen based on reported numbers of users
# and the content of each has not been reviewed, so mileage could vary
peerTubeSites = ('peertube.mastodon.host', 'open.tube', 'share.tube',
'tube.tr4sk.me', 'videos.elbinario.net',
'hkvideo.live',
'peertube.snargol.com', 'tube.22decembre.eu',
'tube.fabrigli.fr', 'libretube.net', 'libre.video',
'peertube.linuxrocks.online', 'spacepub.space',
'video.ploud.jp', 'video.omniatv.com',
'peertube.servebeer.com',
'tube.tchncs.de', 'tubee.fr', 'video.alternanet.fr',
'devtube.dev-wiki.de', 'video.samedi.pm',
'video.irem.univ-paris-diderot.fr',
'peertube.openstreetmap.fr', 'video.antopie.org',
'scitech.video', 'tube.4aem.com', 'video.ploud.fr',
'peervideo.net', 'video.valme.io',
'videos.pair2jeux.tube',
'vault.mle.party', 'hostyour.tv',
'diode.zone', 'visionon.tv',
'artitube.artifaille.fr', 'peertube.fr',
'peertube.live',
'tube.ac-lyon.fr', 'www.yiny.org', 'betamax.video',
'tube.piweb.be', 'pe.ertu.be', 'peertube.social',
'videos.lescommuns.org', 'peertube.nogafa.org',
'skeptikon.fr', 'video.tedomum.net',
'tube.p2p.legal',
'sikke.fi', 'exode.me', 'peertube.video')
for site in peerTubeSites:
if '"https://' + site in content:
url = content.split('"https://' + site)[1]
if '"' in url:
url = url.split('"')[0].replace('/watch/', '/embed/')
content = \
content + "\n\n \n"
return content
return content
def addEmbeddedElements(translate: {}, content: str) -> str:
"""Adds embedded elements for various media types
"""
content = addEmbeddedVideoFromSites(translate, content)
content = addEmbeddedAudio(translate, content)
return addEmbeddedVideo(translate, content)
def followerApprovalActive(baseDir: str, nickname: str, domain: str) -> bool:
"""Returns true if the given account requires follower approval
"""
manuallyApprovesFollowers = False
actorFilename = baseDir + '/accounts/' + nickname + '@' + domain + '.json'
if os.path.isfile(actorFilename):
actorJson = loadJson(actorFilename)
if actorJson:
if actorJson.get('manuallyApprovesFollowers'):
manuallyApprovesFollowers = \
actorJson['manuallyApprovesFollowers']
return manuallyApprovesFollowers
def insertQuestion(baseDir: str, translate: {},
nickname: str, domain: str, port: int,
content: str,
postJsonObject: {}, pageNumber: int) -> str:
""" Inserts question selection into a post
"""
if not isQuestion(postJsonObject):
return content
if len(postJsonObject['object']['oneOf']) == 0:
return content
messageId = removeIdEnding(postJsonObject['id'])
if '#' in messageId:
messageId = messageId.split('#', 1)[0]
pageNumberStr = ''
if pageNumber:
pageNumberStr = '?page=' + str(pageNumber)
votesFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + '/questions.txt'
showQuestionResults = False
if os.path.isfile(votesFilename):
if messageId in open(votesFilename).read():
showQuestionResults = True
if not showQuestionResults:
# show the question options
content += ''
content += \
'
\n'
content += \
' \n \n'
for choice in postJsonObject['object']['oneOf']:
if not choice.get('type'):
continue
if not choice.get('name'):
continue
content += \
' ' + choice['name'] + ' \n'
content += \
' \n'
content += ' \n\n'
else:
# show the responses to a question
content += '\n'
return content
def addEmojiToDisplayName(baseDir: str, httpPrefix: str,
nickname: str, domain: str,
displayName: str, inProfileName: bool) -> str:
"""Adds emoji icons to display names on individual posts
"""
if ':' not in displayName:
return displayName
displayName = displayName.replace('', '').replace('
', '')
emojiTags = {}
print('TAG: displayName before tags: ' + displayName)
displayName = \
addHtmlTags(baseDir, httpPrefix,
nickname, domain, displayName, [], emojiTags)
displayName = displayName.replace('', '').replace('
', '')
print('TAG: displayName after tags: ' + displayName)
# convert the emoji dictionary to a list
emojiTagsList = []
for tagName, tag in emojiTags.items():
emojiTagsList.append(tag)
print('TAG: emoji tags list: ' + str(emojiTagsList))
if not inProfileName:
displayName = \
replaceEmojiFromTags(displayName, emojiTagsList, 'post header')
else:
displayName = \
replaceEmojiFromTags(displayName, emojiTagsList, 'profile')
print('TAG: displayName after tags 2: ' + displayName)
# remove any stray emoji
while ':' in displayName:
if '://' in displayName:
break
emojiStr = displayName.split(':')[1]
prevDisplayName = displayName
displayName = displayName.replace(':' + emojiStr + ':', '').strip()
if prevDisplayName == displayName:
break
print('TAG: displayName after tags 3: ' + displayName)
print('TAG: displayName after tag replacements: ' + displayName)
return displayName
def postContainsPublic(postJsonObject: {}) -> bool:
"""Does the given post contain #Public
"""
containsPublic = False
if not postJsonObject['object'].get('to'):
return containsPublic
for toAddress in postJsonObject['object']['to']:
if toAddress.endswith('#Public'):
containsPublic = True
break
if not containsPublic:
if postJsonObject['object'].get('cc'):
for toAddress in postJsonObject['object']['cc']:
if toAddress.endswith('#Public'):
containsPublic = True
break
return containsPublic
def loadIndividualPostAsHtmlFromCache(baseDir: str,
nickname: str, domain: str,
postJsonObject: {}) -> str:
"""If a cached html version of the given post exists then load it and
return the html text
This is much quicker than generating the html from the json object
"""
cachedPostFilename = \
getCachedPostFilename(baseDir, nickname, domain, postJsonObject)
postHtml = ''
if not cachedPostFilename:
return postHtml
if not os.path.isfile(cachedPostFilename):
return postHtml
tries = 0
while tries < 3:
try:
with open(cachedPostFilename, 'r') as file:
postHtml = file.read()
break
except Exception as e:
print(e)
# no sleep
tries += 1
if postHtml:
return postHtml
def saveIndividualPostAsHtmlToCache(baseDir: str,
nickname: str, domain: str,
postJsonObject: {},
postHtml: str) -> bool:
"""Saves the given html for a post to a cache file
This is so that it can be quickly reloaded on subsequent
refresh of the timeline
"""
htmlPostCacheDir = \
getCachedPostDirectory(baseDir, nickname, domain)
cachedPostFilename = \
getCachedPostFilename(baseDir, nickname, domain, postJsonObject)
# create the cache directory if needed
if not os.path.isdir(htmlPostCacheDir):
os.mkdir(htmlPostCacheDir)
try:
with open(cachedPostFilename, 'w+') as fp:
fp.write(postHtml)
return True
except Exception as e:
print('ERROR: saving post to cache ' + str(e))
return False
def preparePostFromHtmlCache(postHtml: str, boxName: str,
pageNumber: int) -> str:
"""Sets the page number on a cached html post
"""
# if on the bookmarks timeline then remain there
if boxName == 'tlbookmarks' or boxName == 'bookmarks':
postHtml = postHtml.replace('?tl=inbox', '?tl=tlbookmarks')
if '?page=' in postHtml:
pageNumberStr = postHtml.split('?page=')[1]
if '?' in pageNumberStr:
pageNumberStr = pageNumberStr.split('?')[0]
postHtml = postHtml.replace('?page=' + pageNumberStr, '?page=-999')
withPageNumber = postHtml.replace(';-999;', ';' + str(pageNumber) + ';')
withPageNumber = withPageNumber.replace('?page=-999',
'?page=' + str(pageNumber))
return withPageNumber
def postIsMuted(baseDir: str, nickname: str, domain: str,
postJsonObject: {}, messageId: str) -> bool:
""" Returns true if the given post is muted
"""
isMuted = postJsonObject.get('muted')
if isMuted is True or isMuted is False:
return isMuted
postDir = baseDir + '/accounts/' + nickname + '@' + domain
muteFilename = \
postDir + '/inbox/' + messageId.replace('/', '#') + '.json.muted'
if os.path.isfile(muteFilename):
return True
muteFilename = \
postDir + '/outbox/' + messageId.replace('/', '#') + '.json.muted'
if os.path.isfile(muteFilename):
return True
muteFilename = \
baseDir + '/accounts/cache/announce/' + nickname + \
'/' + messageId.replace('/', '#') + '.json.muted'
if os.path.isfile(muteFilename):
return True
return False
def getPostAttachmentsAsHtml(postJsonObject: {}, boxName: str, translate: {},
isMuted: bool, avatarLink: str,
replyStr: str, announceStr: str, likeStr: str,
bookmarkStr: str, deleteStr: str,
muteStr: str) -> (str, str):
"""Returns a string representing any attachments
"""
attachmentStr = ''
galleryStr = ''
if not postJsonObject['object'].get('attachment'):
return attachmentStr, galleryStr
if not isinstance(postJsonObject['object']['attachment'], list):
return attachmentStr, galleryStr
attachmentCtr = 0
attachmentStr += ''
return attachmentStr, galleryStr
def individualPostAsHtml(allowDownloads: bool,
recentPostsCache: {}, maxRecentPosts: int,
iconsDir: str, translate: {},
pageNumber: int, baseDir: str,
session, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int,
postJsonObject: {},
avatarUrl: str, showAvatarOptions: bool,
allowDeletion: bool,
httpPrefix: str, projectVersion: str,
boxName: str, YTReplacementDomain: str,
showRepeats=True,
showIcons=False,
manuallyApprovesFollowers=False,
showPublicOnly=False,
storeToCache=True) -> str:
""" Shows a single post as html
"""
if not postJsonObject:
return ''
# benchmark
postStartTime = time.time()
postActor = postJsonObject['actor']
# ZZZzzz
if isPersonSnoozed(baseDir, nickname, domain, postActor):
return ''
# benchmark 1
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 1 = ' + str(timeDiff))
avatarPosition = ''
messageId = ''
if postJsonObject.get('id'):
messageId = removeIdEnding(postJsonObject['id'])
# benchmark 2
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 2 = ' + str(timeDiff))
messageIdStr = ''
if messageId:
messageIdStr = ';' + messageId
fullDomain = domain
if port:
if port != 80 and port != 443:
if ':' not in domain:
fullDomain = domain + ':' + str(port)
pageNumberParam = ''
if pageNumber:
pageNumberParam = '?page=' + str(pageNumber)
if (not showPublicOnly and
(storeToCache or boxName == 'bookmarks' or
boxName == 'tlbookmarks') and
boxName != 'tlmedia'):
# update avatar if needed
if not avatarUrl:
avatarUrl = \
getPersonAvatarUrl(baseDir, postActor, personCache,
allowDownloads)
# benchmark 2.1
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName +
' 2.1 = ' + str(timeDiff))
updateAvatarImageCache(session, baseDir, httpPrefix,
postActor, avatarUrl, personCache,
allowDownloads)
# benchmark 2.2
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName +
' 2.2 = ' + str(timeDiff))
postHtml = \
loadIndividualPostAsHtmlFromCache(baseDir, nickname, domain,
postJsonObject)
if postHtml:
postHtml = preparePostFromHtmlCache(postHtml, boxName, pageNumber)
updateRecentPostsCache(recentPostsCache, maxRecentPosts,
postJsonObject, postHtml)
# benchmark 3
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName +
' 3 = ' + str(timeDiff))
return postHtml
# benchmark 4
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 4 = ' + str(timeDiff))
if not avatarUrl:
avatarUrl = \
getPersonAvatarUrl(baseDir, postActor, personCache,
allowDownloads)
avatarUrl = \
updateAvatarImageCache(session, baseDir, httpPrefix,
postActor, avatarUrl, personCache,
allowDownloads)
else:
updateAvatarImageCache(session, baseDir, httpPrefix,
postActor, avatarUrl, personCache,
allowDownloads)
# benchmark 5
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 5 = ' + str(timeDiff))
if not avatarUrl:
avatarUrl = postActor + '/avatar.png'
if fullDomain not in postActor:
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
avatarUrl2, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache,
projectVersion, httpPrefix,
nickname, domain, 'outbox')
# benchmark 6
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 6 = ' + str(timeDiff))
if avatarUrl2:
avatarUrl = avatarUrl2
if displayName:
if ':' in displayName:
displayName = \
addEmojiToDisplayName(baseDir, httpPrefix,
nickname, domain,
displayName, False)
# benchmark 7
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 7 = ' + str(timeDiff))
avatarLink = ' '
avatarLink += \
' \n'
if showAvatarOptions and \
fullDomain + '/users/' + nickname not in postActor:
avatarLink = \
' \n'
avatarLink += \
' \n'
avatarImageInPost = \
' ' + avatarLink.strip() + '
\n'
# don't create new html within the bookmarks timeline
# it should already have been created for the inbox
if boxName == 'tlbookmarks' or boxName == 'bookmarks':
return ''
timelinePostBookmark = removeIdEnding(postJsonObject['id'])
timelinePostBookmark = timelinePostBookmark.replace('://', '-')
timelinePostBookmark = timelinePostBookmark.replace('/', '-')
# If this is the inbox timeline then don't show the repeat icon on any DMs
showRepeatIcon = showRepeats
isPublicRepeat = False
showDMicon = False
if showRepeats:
if isDM(postJsonObject):
showDMicon = True
showRepeatIcon = False
else:
if not isPublicPost(postJsonObject):
isPublicRepeat = True
titleStr = ''
galleryStr = ''
isAnnounced = False
if postJsonObject['type'] == 'Announce':
postJsonAnnounce = \
downloadAnnounce(session, baseDir, httpPrefix,
nickname, domain, postJsonObject,
projectVersion, translate,
YTReplacementDomain)
if not postJsonAnnounce:
return ''
postJsonObject = postJsonAnnounce
isAnnounced = True
# benchmark 8
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 8 = ' + str(timeDiff))
if not isinstance(postJsonObject['object'], dict):
return ''
# if this post should be public then check its recipients
if showPublicOnly:
if not postContainsPublic(postJsonObject):
return ''
isModerationPost = False
if postJsonObject['object'].get('moderationStatus'):
isModerationPost = True
containerClass = 'container'
containerClassIcons = 'containericons'
timeClass = 'time-right'
actorNickname = getNicknameFromActor(postActor)
if not actorNickname:
# single user instance
actorNickname = 'dev'
actorDomain, actorPort = getDomainFromActor(postActor)
displayName = getDisplayName(baseDir, postActor, personCache)
if displayName:
if ':' in displayName:
displayName = \
addEmojiToDisplayName(baseDir, httpPrefix,
nickname, domain,
displayName, False)
titleStr += \
' ' + displayName + ' \n'
else:
if not messageId:
# pprint(postJsonObject)
print('ERROR: no messageId')
if not actorNickname:
# pprint(postJsonObject)
print('ERROR: no actorNickname')
if not actorDomain:
# pprint(postJsonObject)
print('ERROR: no actorDomain')
titleStr += \
' @' + actorNickname + '@' + actorDomain + ' \n'
# benchmark 9
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 9 = ' + str(timeDiff))
# Show a DM icon for DMs in the inbox timeline
if showDMicon:
titleStr = \
titleStr + ' \n'
replyStr = ''
# check if replying is permitted
commentsEnabled = True
if 'commentsEnabled' in postJsonObject['object']:
if postJsonObject['object']['commentsEnabled'] is False:
commentsEnabled = False
if showIcons and commentsEnabled:
# reply is permitted - create reply icon
replyToLink = postJsonObject['object']['id']
if postJsonObject['object'].get('attributedTo'):
if isinstance(postJsonObject['object']['attributedTo'], str):
replyToLink += \
'?mention=' + postJsonObject['object']['attributedTo']
if postJsonObject['object'].get('content'):
mentionedActors = \
getMentionsFromHtml(postJsonObject['object']['content'])
if mentionedActors:
for actorUrl in mentionedActors:
if '?mention=' + actorUrl not in replyToLink:
replyToLink += '?mention=' + actorUrl
if len(replyToLink) > 500:
break
replyToLink += pageNumberParam
replyStr = ''
if isPublicRepeat:
replyStr += \
' \n'
else:
if isDM(postJsonObject):
replyStr += \
' ' + \
' \n'
else:
replyStr += \
' ' + \
' \n'
replyStr += \
' ' + \
' \n'
# benchmark 10
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 10 = ' + str(timeDiff))
isEvent = isEventPost(postJsonObject)
# benchmark 11
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 11 = ' + str(timeDiff))
editStr = ''
if fullDomain + '/users/' + nickname in postJsonObject['actor']:
if '/statuses/' in postJsonObject['object']['id']:
if isBlogPost(postJsonObject):
blogPostId = postJsonObject['object']['id']
editStr += \
' ' + \
'' + \
' \n'
elif isEvent:
eventPostId = postJsonObject['object']['id']
editStr += \
' ' + \
'' + \
' \n'
announceStr = ''
if not isModerationPost and showRepeatIcon:
# don't allow announce/repeat of your own posts
announceIcon = 'repeat_inactive.png'
announceLink = 'repeat'
if not isPublicRepeat:
announceLink = 'repeatprivate'
announceTitle = translate['Repeat this post']
if announcedByPerson(postJsonObject, nickname, fullDomain):
announceIcon = 'repeat.png'
if not isPublicRepeat:
announceLink = 'unrepeatprivate'
announceTitle = translate['Undo the repeat']
announceStr = \
' \n'
announceStr += \
' ' + \
' \n'
# benchmark 12
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 12 = ' + str(timeDiff))
# whether to show a like button
hideLikeButtonFile = \
baseDir + '/accounts/' + nickname + '@' + domain + '/.hideLikeButton'
showLikeButton = True
if os.path.isfile(hideLikeButtonFile):
showLikeButton = False
likeStr = ''
if not isModerationPost and showLikeButton:
likeIcon = 'like_inactive.png'
likeLink = 'like'
likeTitle = translate['Like this post']
likeCount = noOfLikes(postJsonObject)
# benchmark 12.1
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 12.1 = ' + str(timeDiff))
likeCountStr = ''
if likeCount > 0:
if likeCount <= 10:
likeCountStr = ' (' + str(likeCount) + ')'
else:
likeCountStr = ' (10+)'
if likedByPerson(postJsonObject, nickname, fullDomain):
if likeCount == 1:
# liked by the reader only
likeCountStr = ''
likeIcon = 'like.png'
likeLink = 'unlike'
likeTitle = translate['Undo the like']
# benchmark 12.2
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 12.2 = ' + str(timeDiff))
likeStr = ''
if likeCountStr:
# show the number of likes next to icon
likeStr += ''
likeStr += likeCountStr.replace('(', '').replace(')', '').strip()
likeStr += ' \n'
likeStr += \
' \n'
likeStr += \
' ' + \
' \n'
# benchmark 12.5
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 12.5 = ' + str(timeDiff))
bookmarkStr = ''
if not isModerationPost:
bookmarkIcon = 'bookmark_inactive.png'
bookmarkLink = 'bookmark'
bookmarkTitle = translate['Bookmark this post']
if bookmarkedByPerson(postJsonObject, nickname, fullDomain):
bookmarkIcon = 'bookmark.png'
bookmarkLink = 'unbookmark'
bookmarkTitle = translate['Undo the bookmark']
# benchmark 12.6
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 12.6 = ' + str(timeDiff))
bookmarkStr = \
' \n'
bookmarkStr += \
' ' + \
' \n'
# benchmark 12.9
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 12.9 = ' + str(timeDiff))
isMuted = postIsMuted(baseDir, nickname, domain, postJsonObject, messageId)
# benchmark 13
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 13 = ' + str(timeDiff))
deleteStr = ''
muteStr = ''
if (allowDeletion or
('/' + fullDomain + '/' in postActor and
messageId.startswith(postActor))):
if '/users/' + nickname + '/' in messageId:
deleteStr = \
' \n'
deleteStr += \
' ' + \
' \n'
else:
if not isMuted:
muteStr = \
' \n'
muteStr += \
' ' + \
' \n'
else:
muteStr = \
' \n'
muteStr += \
' ' + \
' \n'
# benchmark 13.1
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 13.1 = ' + str(timeDiff))
replyAvatarImageInPost = ''
if showRepeatIcon:
if isAnnounced:
if postJsonObject['object'].get('attributedTo'):
attributedTo = ''
if isinstance(postJsonObject['object']['attributedTo'], str):
attributedTo = postJsonObject['object']['attributedTo']
if attributedTo.startswith(postActor):
titleStr += \
' \n'
else:
# benchmark 13.2
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName +
' 13.2 = ' + str(timeDiff))
announceNickname = None
if attributedTo:
announceNickname = getNicknameFromActor(attributedTo)
if announceNickname:
announceDomain, announcePort = \
getDomainFromActor(attributedTo)
getPersonFromCache(baseDir, attributedTo,
personCache, allowDownloads)
announceDisplayName = \
getDisplayName(baseDir, attributedTo, personCache)
if announceDisplayName:
# benchmark 13.3
if not allowDownloads:
timeDiff = \
int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName +
' 13.3 = ' + str(timeDiff))
if ':' in announceDisplayName:
announceDisplayName = \
addEmojiToDisplayName(baseDir, httpPrefix,
nickname, domain,
announceDisplayName,
False)
# benchmark 13.3.1
if not allowDownloads:
timeDiff = \
int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName +
' 13.3.1 = ' + str(timeDiff))
titleStr += \
' ' + \
' \n' + \
' ' + \
announceDisplayName + ' \n'
# show avatar of person replied to
announceActor = \
postJsonObject['object']['attributedTo']
announceAvatarUrl = \
getPersonAvatarUrl(baseDir, announceActor,
personCache, allowDownloads)
# benchmark 13.4
if not allowDownloads:
timeDiff = \
int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName +
' 13.4 = ' + str(timeDiff))
if announceAvatarUrl:
idx = 'Show options for this person'
replyAvatarImageInPost = \
' ' \
'\n'
else:
titleStr += \
' \n' + \
' @' + \
announceNickname + '@' + \
announceDomain + ' \n'
else:
titleStr += \
' \n' + \
' @unattributed \n'
else:
titleStr += \
' ' + \
' \n' + \
' @unattributed \n'
else:
if postJsonObject['object'].get('inReplyTo'):
containerClassIcons = 'containericons darker'
containerClass = 'container darker'
if postJsonObject['object']['inReplyTo'].startswith(postActor):
titleStr += \
' \n'
else:
if '/statuses/' in postJsonObject['object']['inReplyTo']:
inReplyTo = postJsonObject['object']['inReplyTo']
replyActor = inReplyTo.split('/statuses/')[0]
replyNickname = getNicknameFromActor(replyActor)
if replyNickname:
replyDomain, replyPort = \
getDomainFromActor(replyActor)
if replyNickname and replyDomain:
getPersonFromCache(baseDir, replyActor,
personCache,
allowDownloads)
replyDisplayName = \
getDisplayName(baseDir, replyActor,
personCache)
if replyDisplayName:
if ':' in replyDisplayName:
# benchmark 13.5
if not allowDownloads:
timeDiff = \
int((time.time() -
postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' +
boxName + ' 13.5 = ' +
str(timeDiff))
repDisp = replyDisplayName
replyDisplayName = \
addEmojiToDisplayName(baseDir,
httpPrefix,
nickname,
domain,
repDisp,
False)
# benchmark 13.6
if not allowDownloads:
timeDiff = \
int((time.time() -
postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' +
boxName + ' 13.6 = ' +
str(timeDiff))
titleStr += \
' ' + \
' \n' + \
' ' + \
'' + replyDisplayName + ' \n'
# benchmark 13.7
if not allowDownloads:
timeDiff = int((time.time() -
postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName +
' 13.7 = ' + str(timeDiff))
# show avatar of person replied to
replyAvatarUrl = \
getPersonAvatarUrl(baseDir,
replyActor,
personCache,
allowDownloads)
# benchmark 13.8
if not allowDownloads:
timeDiff = int((time.time() -
postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName +
' 13.8 = ' + str(timeDiff))
if replyAvatarUrl:
replyAvatarImageInPost = \
' \n'
else:
inReplyTo = \
postJsonObject['object']['inReplyTo']
titleStr += \
' ' + \
' \n' + \
' @' + \
replyNickname + '@' + \
replyDomain + ' \n'
else:
titleStr += \
' \n' + \
' @unknown \n'
else:
postDomain = \
postJsonObject['object']['inReplyTo']
prefixes = getProtocolPrefixes()
for prefix in prefixes:
postDomain = postDomain.replace(prefix, '')
if '/' in postDomain:
postDomain = postDomain.split('/', 1)[0]
if postDomain:
titleStr += \
' \n' + \
' ' + postDomain + ' \n'
# benchmark 14
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 14 = ' + str(timeDiff))
attachmentStr, galleryStr = \
getPostAttachmentsAsHtml(postJsonObject, boxName, translate,
isMuted, avatarLink.strip(),
replyStr, announceStr, likeStr,
bookmarkStr, deleteStr, muteStr)
publishedStr = ''
if postJsonObject['object'].get('published'):
publishedStr = postJsonObject['object']['published']
if '.' not in publishedStr:
if '+' not in publishedStr:
datetimeObject = \
datetime.strptime(publishedStr, "%Y-%m-%dT%H:%M:%SZ")
else:
datetimeObject = \
datetime.strptime(publishedStr.split('+')[0] + 'Z',
"%Y-%m-%dT%H:%M:%SZ")
else:
publishedStr = \
publishedStr.replace('T', ' ').split('.')[0]
datetimeObject = parse(publishedStr)
publishedStr = datetimeObject.strftime("%a %b %d, %H:%M")
# benchmark 15
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 15 = ' + str(timeDiff))
publishedLink = messageId
# blog posts should have no /statuses/ in their link
if isBlogPost(postJsonObject):
# is this a post to the local domain?
if '://' + domain in messageId:
publishedLink = messageId.replace('/statuses/', '/')
# if this is a local link then make it relative so that it works
# on clearnet or onion address
if domain + '/users/' in publishedLink or \
domain + ':' + str(port) + '/users/' in publishedLink:
publishedLink = '/users/' + publishedLink.split('/users/')[1]
footerStr = '' + publishedStr + ' \n'
# change the background color for DMs in inbox timeline
if showDMicon:
containerClassIcons = 'containericons dm'
containerClass = 'container dm'
if showIcons:
footerStr = '\n \n'
footerStr += replyStr + announceStr + likeStr + bookmarkStr + \
deleteStr + muteStr + editStr
footerStr += '
' + publishedStr + ' \n'
footerStr += '
\n'
postIsSensitive = False
if postJsonObject['object'].get('sensitive'):
# sensitive posts should have a summary
if postJsonObject['object'].get('summary'):
postIsSensitive = postJsonObject['object']['sensitive']
else:
# add a generic summary if none is provided
postJsonObject['object']['summary'] = translate['Sensitive']
# add an extra line if there is a content warning,
# for better vertical spacing on mobile
if postIsSensitive:
footerStr = ' ' + footerStr
if not postJsonObject['object'].get('summary'):
postJsonObject['object']['summary'] = ''
if postJsonObject['object'].get('cipherText'):
postJsonObject['object']['content'] = \
E2EEdecryptMessageFromDevice(postJsonObject['object'])
if not postJsonObject['object'].get('content'):
return ''
isPatch = isGitPatch(baseDir, nickname, domain,
postJsonObject['object']['type'],
postJsonObject['object']['summary'],
postJsonObject['object']['content'])
# benchmark 16
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 16 = ' + str(timeDiff))
if not isPatch:
objectContent = \
removeLongWords(postJsonObject['object']['content'], 40, [])
objectContent = removeTextFormatting(objectContent)
objectContent = \
switchWords(baseDir, nickname, domain, objectContent)
objectContent = htmlReplaceEmailQuote(objectContent)
objectContent = htmlReplaceQuoteMarks(objectContent)
else:
objectContent = \
postJsonObject['object']['content']
if not postIsSensitive:
contentStr = objectContent + attachmentStr
contentStr = addEmbeddedElements(translate, contentStr)
contentStr = insertQuestion(baseDir, translate,
nickname, domain, port,
contentStr, postJsonObject,
pageNumber)
else:
postID = 'post' + str(createPassword(8))
contentStr = ''
if postJsonObject['object'].get('summary'):
contentStr += \
'' + postJsonObject['object']['summary'] + ' \n '
if isModerationPost:
containerClass = 'container report'
# get the content warning text
cwContentStr = objectContent + attachmentStr
if not isPatch:
cwContentStr = addEmbeddedElements(translate, cwContentStr)
cwContentStr = \
insertQuestion(baseDir, translate, nickname, domain, port,
cwContentStr, postJsonObject, pageNumber)
if not isBlogPost(postJsonObject):
# get the content warning button
contentStr += \
getContentWarningButton(postID, translate, cwContentStr)
else:
contentStr += cwContentStr
# benchmark 17
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 17 = ' + str(timeDiff))
if postJsonObject['object'].get('tag') and not isPatch:
contentStr = \
replaceEmojiFromTags(contentStr,
postJsonObject['object']['tag'],
'content')
if isMuted:
contentStr = ''
else:
if not isPatch:
contentStr = ' ' + \
contentStr + \
'
\n'
else:
contentStr = \
'\n'
postHtml = ''
if boxName != 'tlmedia':
postHtml = ' \n'
postHtml += avatarImageInPost
postHtml += '
\n' + \
' ' + titleStr + \
replyAvatarImageInPost + '
\n'
postHtml += contentStr + footerStr + '\n'
postHtml += '
\n'
else:
postHtml = galleryStr
# benchmark 18
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 18 = ' + str(timeDiff))
if not showPublicOnly and storeToCache and \
boxName != 'tlmedia' and boxName != 'tlbookmarks' and \
boxName != 'bookmarks':
saveIndividualPostAsHtmlToCache(baseDir, nickname, domain,
postJsonObject, postHtml)
updateRecentPostsCache(recentPostsCache, maxRecentPosts,
postJsonObject, postHtml)
# benchmark 19
if not allowDownloads:
timeDiff = int((time.time() - postStartTime) * 1000)
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 19 = ' + str(timeDiff))
return postHtml
def isQuestion(postObjectJson: {}) -> bool:
""" is the given post a question?
"""
if postObjectJson['type'] != 'Create' and \
postObjectJson['type'] != 'Update':
return False
if not isinstance(postObjectJson['object'], dict):
return False
if not postObjectJson['object'].get('type'):
return False
if postObjectJson['object']['type'] != 'Question':
return False
if not postObjectJson['object'].get('oneOf'):
return False
if not isinstance(postObjectJson['object']['oneOf'], list):
return False
return True
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 '*' + label + '*'
def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str,
httpPrefix: str, translate: {},
iconsDir: str, moderator: bool) -> str:
"""Returns html content for the left column
"""
htmlStr = ''
domain = domainFull
if ':' in domain:
domain = domain.split(':')
leftColumnImageFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + \
'/left_col_image.png'
if not os.path.isfile(leftColumnImageFilename):
theme = getConfigParam(baseDir, 'theme').lower()
if theme == 'default':
theme = ''
else:
theme = '_' + theme
themeLeftColumnImageFilename = \
baseDir + '/img/left_col_image' + theme + '.png'
if os.path.isfile(themeLeftColumnImageFilename):
copyfile(themeLeftColumnImageFilename, leftColumnImageFilename)
# show the image at the top of the column
editImageClass = 'leftColEdit'
if os.path.isfile(leftColumnImageFilename):
editImageClass = 'leftColEditImage'
htmlStr += \
'\n \n' + \
' \n' + \
' \n'
if editImageClass == 'leftColEdit':
htmlStr += '\n \n'
if moderator:
# show the edit icon
htmlStr += \
' ' + \
' \n'
# RSS icon
htmlStr += \
' ' + \
' \n'
if editImageClass == 'leftColEdit':
htmlStr += ' \n'
else:
htmlStr += ' \n'
linksFilename = baseDir + '/accounts/links.txt'
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'
else:
if lineStr.startswith('#') or lineStr.startswith('*'):
lineStr = lineStr[1:].strip()
htmlStr += \
' \n'
else:
htmlStr += \
' ' + lineStr + '
\n'
return htmlStr
def htmlNewswire(newswire: str, nickname: str, moderator: bool,
translate: {}) -> str:
"""Converts a newswire dict into html
"""
htmlStr = ''
for dateStr, item in newswire.items():
dateStrLink = dateStr.replace(' ', 'T')
dateStrLink = dateStrLink.replace('+00:00', '')
if 'vote:' + nickname in item[2]:
totalVotesStr = ''
if moderator:
# count the total votes for this item
totalVotes = 0
for line in item[2]:
if 'vote:' in line:
totalVotes += 1
if totalVotes > 0:
totalVotesStr = ' +' + str(totalVotes)
htmlStr += '' + \
'' + item[0] + ' ' + \
totalVotesStr
if moderator:
htmlStr += \
' ' + \
'' + \
''
htmlStr += dateStr.replace('+00:00', '') + '
'
else:
htmlStr += ' '
htmlStr += dateStr.replace('+00:00', '') + ' '
else:
totalVotesStr = ''
if moderator:
# count the total votes for this item
totalVotes = 0
for line in item[2]:
if 'vote:' in line:
totalVotes += 1
if totalVotes > 0:
totalVotesStr = ' +' + str(totalVotes)
htmlStr += '' + \
'' + item[0] + ' ' + \
totalVotesStr
if moderator:
htmlStr += \
' ' + \
'' + \
''
htmlStr += dateStr.replace('+00:00', '') + '
'
else:
htmlStr += ' '
htmlStr += dateStr.replace('+00:00', '') + ' '
return htmlStr
def getRightColumnContent(baseDir: str, nickname: str, domainFull: str,
httpPrefix: str, translate: {},
iconsDir: str, moderator: bool,
newswire: {}) -> str:
"""Returns html content for the right column
"""
htmlStr = ''
domain = domainFull
if ':' in domain:
domain = domain.split(':')
rightColumnImageFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + \
'/right_col_image.png'
if not os.path.isfile(rightColumnImageFilename):
theme = getConfigParam(baseDir, 'theme').lower()
if theme == 'default':
theme = ''
else:
theme = '_' + theme
themeRightColumnImageFilename = \
baseDir + '/img/right_col_image' + theme + '.png'
if os.path.isfile(themeRightColumnImageFilename):
copyfile(themeRightColumnImageFilename, rightColumnImageFilename)
# show the image at the top of the column
editImageClass = 'rightColEdit'
if os.path.isfile(rightColumnImageFilename):
editImageClass = 'rightColEditImage'
htmlStr += \
'\n \n' + \
' \n' + \
' \n'
if editImageClass == 'rightColEdit':
htmlStr += '\n \n'
if moderator:
if os.path.isfile(baseDir + '/accounts/newswiremoderation.txt'):
# show the edit icon highlighted
htmlStr += \
' ' + \
' \n'
else:
# show the edit icon
htmlStr += \
' ' + \
' \n'
htmlStr += \
' ' + \
' \n'
if editImageClass == 'rightColEdit':
htmlStr += ' \n'
else:
htmlStr += ' \n'
htmlStr += htmlNewswire(newswire, nickname, moderator, translate)
return htmlStr
def htmlTimeline(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,
newswire: {}) -> 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)
# 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 = 'banner.png'
bannerFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + bannerFile
if not os.path.isfile(bannerFilename):
bannerFile = 'banner.jpg'
bannerFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + bannerFile
if not os.path.isfile(bannerFilename):
bannerFile = 'banner.gif'
bannerFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + bannerFile
if not os.path.isfile(bannerFilename):
bannerFile = 'banner.avif'
bannerFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + bannerFile
if not os.path.isfile(bannerFilename):
bannerFile = 'banner.webp'
# benchmark 1
timeDiff = int((time.time() - timelineStartTime) * 1000)
if timeDiff > 100:
print('TIMELINE TIMING ' + boxName + ' 1 = ' + str(timeDiff))
with open(cssFilename, 'r') as cssFile:
# load css
profileStyle = \
cssFile.read().replace('banner.png',
'/users/' + nickname + '/' + bannerFile)
# replace any https within the css with whatever prefix is needed
if httpPrefix != 'https':
profileStyle = \
profileStyle.replace('https://',
httpPrefix + '://')
# is the user a moderator?
moderator = isModerator(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 = \
'' + \
' \n'
break
# benchmark 3
timeDiff = int((time.time() - timelineStartTime) * 1000)
if timeDiff > 100:
print('TIMELINE TIMING ' + boxName + ' 3 = ' + str(timeDiff))
# moderation / reports button
moderationButtonStr = ''
if moderator and not minimal:
moderationButtonStr = \
'' + \
htmlHighlightLabel(translate['Mod'], newReport) + \
' \n'
# shares, bookmarks and events buttons
sharesButtonStr = ''
bookmarksButtonStr = ''
eventsButtonStr = ''
if not minimal:
sharesButtonStr = \
'' + \
htmlHighlightLabel(translate['Shares'], newShare) + \
' \n'
bookmarksButtonStr = \
'' + translate['Bookmarks'] + \
' \n'
eventsButtonStr = \
'' + translate['Events'] + \
' \n'
tlStr = htmlHeader(cssFilename, profileStyle)
# benchmark 4
timeDiff = int((time.time() - timelineStartTime) * 1000)
if timeDiff > 100:
print('TIMELINE TIMING ' + boxName + ' 4 = ' + str(timeDiff))
# what screen to go to when a new post is created
if boxName == 'dm':
newPostButtonStr = \
' \n'
elif boxName == 'tlblogs' or boxName == 'tlnews':
newPostButtonStr = \
' \n'
elif boxName == 'tlevents':
newPostButtonStr = \
' \n'
else:
if not manuallyApproveFollowers:
newPostButtonStr = \
' \n'
else:
newPostButtonStr = \
' \n'
# This creates a link to the profile page when viewed
# in lynx, but should be invisible in a graphical web browser
tlStr += \
'' + \
translate['Switch to profile view'] + ' \n'
# banner and row of buttons
tlStr += \
'\n'
tlStr += ''
tlStr += '
\n \n'
# start the timeline
tlStr += '\n'
tlStr += ' \n'
tlStr += ' \n'
tlStr += ' \n'
tlStr += ' \n'
tlStr += ' \n'
tlStr += ' \n'
tlStr += ' \n'
domainFull = domain
if port:
if port != 80 and port != 443:
domainFull = domain + ':' + str(port)
# left column
leftColumnStr = \
getLeftColumnContent(baseDir, nickname, domainFull,
httpPrefix, translate, iconsDir,
moderator)
tlStr += ' ' + \
leftColumnStr + ' \n'
# center column containing posts
tlStr += ' \n'
# start of the button header with inbox, outbox, etc
tlStr += ' \n'
# first button
if defaultTimeline == 'tlmedia':
tlStr += \
'
' + translate['Media'] + \
' \n'
elif defaultTimeline == 'tlblogs':
tlStr += \
'
' + translate['Blogs'] + \
' \n'
elif defaultTimeline == 'tlnews':
tlStr += \
'
' + translate['News'] + \
' \n'
else:
tlStr += \
'
' + \
translate['Inbox'] + ' \n'
tlStr += \
'
' + htmlHighlightLabel(translate['DM'], newDM) + \
' \n'
tlStr += \
'
' + \
htmlHighlightLabel(translate['Replies'], newReply) + \
' \n'
# typically the media button
if defaultTimeline != 'tlmedia':
if not minimal:
tlStr += \
'
' + translate['Media'] + \
' \n'
else:
if not minimal:
tlStr += \
'
' + translate['Inbox'] + \
' \n'
# typically the blogs button
# but may change if this is a blogging oriented instance
if defaultTimeline != 'tlblogs':
if not minimal:
tlStr += \
'
' + translate['Blogs'] + \
' \n'
else:
if not minimal:
tlStr += \
'
' + translate['Inbox'] + \
' \n'
# typically the news button
# but may change if this is a news oriented instance
if defaultTimeline != 'tlnews':
if not minimal:
tlStr += \
'
' + translate['News'] + \
' \n'
else:
if not minimal:
tlStr += \
'
' + translate['Inbox'] + \
' \n'
# button for the outbox
tlStr += \
'
' + translate['Outbox'] + \
' \n'
# add other buttons
tlStr += \
sharesButtonStr + bookmarksButtonStr + eventsButtonStr + \
moderationButtonStr + newPostButtonStr
# show todays events buttons on the first inbox page
if boxName == 'inbox' and pageNumber == 1:
if todaysEventsCheck(baseDir, nickname, domain):
now = datetime.now()
# happening today button
tlStr += \
'
' + \
translate['Happening Today'] + ' \n'
# happening this week button
if thisWeeksEventsCheck(baseDir, nickname, domain):
tlStr += \
'
' + \
translate['Happening This Week'] + ' \n'
else:
# happening this week button
if thisWeeksEventsCheck(baseDir, nickname, domain):
tlStr += \
'
' + \
translate['Happening This Week'] + ' \n'
# the search button
tlStr += \
'
\n'
# benchmark 5
timeDiff = int((time.time() - timelineStartTime) * 1000)
if timeDiff > 100:
print('TIMELINE TIMING ' + boxName + ' 5 = ' + str(timeDiff))
# the calendar button
calendarAltText = translate['Calendar']
if newCalendarEvent:
# indicate that the calendar icon is highlighted
calendarAltText = '*' + calendarAltText + '*'
tlStr += \
'
\n'
# the show/hide button, for a simpler header appearance
tlStr += \
'
\n'
tlStr += followApprovals
# end of the button header with inbox, outbox, etc
tlStr += '
\n'
# second row of buttons for moderator actions
if moderator and boxName == 'moderation':
tlStr += \
''
tlStr += '\n'
idx = 'Nickname or URL. Block using *@domain or nickname@domain'
tlStr += \
' ' + translate[idx] + ' \n'
tlStr += ' \n'
tlStr += \
' \n'
tlStr += \
' \n'
tlStr += \
' \n'
tlStr += \
' \n'
tlStr += \
' \n'
tlStr += \
' \n'
tlStr += '
\n \n'
# benchmark 6
timeDiff = int((time.time() - timelineStartTime) * 1000)
if timeDiff > 100:
print('TIMELINE TIMING ' + boxName + ' 6 = ' + str(timeDiff))
if boxName == 'tlshares':
maxSharesPerAccount = itemsPerPage
return (tlStr +
htmlSharesTimeline(translate, pageNumber, itemsPerPage,
baseDir, actor, nickname, domain, port,
maxSharesPerAccount, httpPrefix) +
htmlFooter())
# benchmark 7
timeDiff = int((time.time() - timelineStartTime) * 1000)
if timeDiff > 100:
print('TIMELINE TIMING ' + boxName + ' 7 = ' + str(timeDiff))
# benchmark 8
timeDiff = int((time.time() - timelineStartTime) * 1000)
if timeDiff > 100:
print('TIMELINE TIMING ' + boxName + ' 8 = ' + str(timeDiff))
# page up arrow
if pageNumber > 1:
tlStr += \
' \n' + \
' \n' + \
' \n'
# show the posts
itemCtr = 0
if timelineJson:
# if this is the media timeline then add an extra gallery container
if boxName == 'tlmedia':
if pageNumber > 1:
tlStr += ' '
tlStr += '\n'
# show each post in the timeline
for item in timelineJson['orderedItems']:
timelinePostStartTime = time.time()
if item['type'] == 'Create' or \
item['type'] == 'Announce' or \
item['type'] == 'Update':
# is the actor who sent this post snoozed?
if isPersonSnoozed(baseDir, nickname, domain, item['actor']):
continue
# is the post in the memory cache of recent ones?
currTlStr = None
if boxName != 'tlmedia' and \
recentPostsCache.get('index'):
postId = \
removeIdEnding(item['id']).replace('/', '#')
if postId in recentPostsCache['index']:
if not item.get('muted'):
if recentPostsCache['html'].get(postId):
currTlStr = recentPostsCache['html'][postId]
currTlStr = \
preparePostFromHtmlCache(currTlStr,
boxName,
pageNumber)
# benchmark cache post
timeDiff = \
int((time.time() -
timelinePostStartTime) * 1000)
if timeDiff > 100:
print('TIMELINE POST CACHE TIMING ' +
boxName + ' = ' + str(timeDiff))
if not currTlStr:
# benchmark cache post
timeDiff = \
int((time.time() -
timelinePostStartTime) * 1000)
if timeDiff > 100:
print('TIMELINE POST DISK TIMING START ' +
boxName + ' = ' + str(timeDiff))
# read the post from disk
currTlStr = \
individualPostAsHtml(False, recentPostsCache,
maxRecentPosts,
iconsDir, translate, pageNumber,
baseDir, session, wfRequest,
personCache,
nickname, domain, port,
item, None, True,
allowDeletion,
httpPrefix, projectVersion,
boxName,
YTReplacementDomain,
boxName != 'dm',
showIndividualPostIcons,
manuallyApproveFollowers,
False, True)
# benchmark cache post
timeDiff = \
int((time.time() -
timelinePostStartTime) * 1000)
if timeDiff > 100:
print('TIMELINE POST DISK TIMING ' +
boxName + ' = ' + str(timeDiff))
if currTlStr:
itemCtr += 1
tlStr += currTlStr
if boxName == 'tlmedia':
tlStr += '
\n'
# end of column-center
tlStr += ' \n'
# right column
rightColumnStr = getRightColumnContent(baseDir, nickname, domainFull,
httpPrefix, translate, iconsDir,
moderator, newswire)
tlStr += ' ' + \
rightColumnStr + ' \n'
tlStr += ' \n'
# benchmark 9
timeDiff = int((time.time() - timelineStartTime) * 1000)
if timeDiff > 100:
print('TIMELINE TIMING ' + boxName + ' 9 = ' + str(timeDiff))
# page down arrow
if itemCtr > 2:
tlStr += \
' \n' + \
' \n' + \
' \n' + \
' \n' + \
' \n' + \
' \n' + \
' \n' + \
' \n' + \
' \n'
tlStr += ' \n'
tlStr += '
\n'
tlStr += htmlFooter()
return tlStr
def htmlShares(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int,
allowDeletion: bool,
httpPrefix: str, projectVersion: str,
YTReplacementDomain: str,
newswire: {}) -> str:
"""Show the shares timeline as html
"""
manuallyApproveFollowers = \
followerApprovalActive(baseDir, nickname, domain)
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, None,
'tlshares', allowDeletion,
httpPrefix, projectVersion, manuallyApproveFollowers,
False, YTReplacementDomain, newswire)
def htmlInbox(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, inboxJson: {},
allowDeletion: bool,
httpPrefix: str, projectVersion: str,
minimal: bool, YTReplacementDomain: str,
newswire: {}) -> str:
"""Show the inbox as html
"""
manuallyApproveFollowers = \
followerApprovalActive(baseDir, nickname, domain)
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, inboxJson,
'inbox', allowDeletion,
httpPrefix, projectVersion, manuallyApproveFollowers,
minimal, YTReplacementDomain, newswire)
def htmlBookmarks(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, bookmarksJson: {},
allowDeletion: bool,
httpPrefix: str, projectVersion: str,
minimal: bool, YTReplacementDomain: str,
newswire: {}) -> str:
"""Show the bookmarks as html
"""
manuallyApproveFollowers = \
followerApprovalActive(baseDir, nickname, domain)
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, bookmarksJson,
'tlbookmarks', allowDeletion,
httpPrefix, projectVersion, manuallyApproveFollowers,
minimal, YTReplacementDomain, newswire)
def htmlEvents(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, bookmarksJson: {},
allowDeletion: bool,
httpPrefix: str, projectVersion: str,
minimal: bool, YTReplacementDomain: str,
newswire: {}) -> str:
"""Show the events as html
"""
manuallyApproveFollowers = \
followerApprovalActive(baseDir, nickname, domain)
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, bookmarksJson,
'tlevents', allowDeletion,
httpPrefix, projectVersion, manuallyApproveFollowers,
minimal, YTReplacementDomain, newswire)
def htmlInboxDMs(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, inboxJson: {},
allowDeletion: bool,
httpPrefix: str, projectVersion: str,
minimal: bool, YTReplacementDomain: str,
newswire: {}) -> str:
"""Show the DM timeline as html
"""
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, inboxJson, 'dm', allowDeletion,
httpPrefix, projectVersion, False, minimal,
YTReplacementDomain, newswire)
def htmlInboxReplies(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, inboxJson: {},
allowDeletion: bool,
httpPrefix: str, projectVersion: str,
minimal: bool, YTReplacementDomain: str,
newswire: {}) -> str:
"""Show the replies timeline as html
"""
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, inboxJson, 'tlreplies',
allowDeletion, httpPrefix, projectVersion, False,
minimal, YTReplacementDomain, newswire)
def htmlInboxMedia(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, inboxJson: {},
allowDeletion: bool,
httpPrefix: str, projectVersion: str,
minimal: bool, YTReplacementDomain: str,
newswire: {}) -> str:
"""Show the media timeline as html
"""
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, inboxJson, 'tlmedia',
allowDeletion, httpPrefix, projectVersion, False,
minimal, YTReplacementDomain, newswire)
def htmlInboxBlogs(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, inboxJson: {},
allowDeletion: bool,
httpPrefix: str, projectVersion: str,
minimal: bool, YTReplacementDomain: str,
newswire: {}) -> str:
"""Show the blogs timeline as html
"""
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, inboxJson, 'tlblogs',
allowDeletion, httpPrefix, projectVersion, False,
minimal, YTReplacementDomain, newswire)
def htmlInboxNews(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, inboxJson: {},
allowDeletion: bool,
httpPrefix: str, projectVersion: str,
minimal: bool, YTReplacementDomain: str,
newswire: {}) -> str:
"""Show the news timeline as html
"""
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, inboxJson, 'tlnews',
allowDeletion, httpPrefix, projectVersion, False,
minimal, YTReplacementDomain, newswire)
def htmlModeration(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, inboxJson: {},
allowDeletion: bool,
httpPrefix: str, projectVersion: str,
YTReplacementDomain: str,
newswire: {}) -> str:
"""Show the moderation feed as html
"""
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, inboxJson, 'moderation',
allowDeletion, httpPrefix, projectVersion, True, False,
YTReplacementDomain, newswire)
def htmlOutbox(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, outboxJson: {},
allowDeletion: bool,
httpPrefix: str, projectVersion: str,
minimal: bool, YTReplacementDomain: str,
newswire: {}) -> str:
"""Show the Outbox as html
"""
manuallyApproveFollowers = \
followerApprovalActive(baseDir, nickname, domain)
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, outboxJson, 'outbox',
allowDeletion, httpPrefix, projectVersion,
manuallyApproveFollowers, minimal,
YTReplacementDomain, newswire)
def htmlIndividualPost(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) -> 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'
followStr += \
' \n'
followStr += \
' \n'
if not isFollowingActor(baseDir, nickname, domainFull, likedBy):
followStr += ' ' + translate['Follow'] + ' \n'
followStr += ' ' + translate['Go Back'] + ' \n'
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,
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,
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,
False, authorized,
False, False, False)
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
with open(cssFilename, 'r') as cssFile:
postsCSS = cssFile.read()
if httpPrefix != 'https':
postsCSS = postsCSS.replace('https://',
httpPrefix + '://')
return htmlHeader(cssFilename, postsCSS) + postStr + htmlFooter()
def htmlPostReplies(recentPostsCache: {}, maxRecentPosts: int,
translate: {}, baseDir: str,
session, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, repliesJson: {},
httpPrefix: str, projectVersion: str,
YTReplacementDomain: str) -> 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,
False, False, False, False, False)
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
with open(cssFilename, 'r') as cssFile:
postsCSS = cssFile.read()
if httpPrefix != 'https':
postsCSS = postsCSS.replace('https://',
httpPrefix + '://')
return htmlHeader(cssFilename, postsCSS) + repliesStr + htmlFooter()
def htmlRemoveSharedItem(translate: {}, baseDir: str,
actor: str, shareName: str,
callingDomain: str) -> str:
"""Shows a screen asking to confirm the removal of a shared item
"""
itemID = getValidSharedItemID(shareName)
nickname = getNicknameFromActor(actor)
domain, port = getDomainFromActor(actor)
domainFull = domain
if port:
if port != 80 and port != 443:
domainFull = domain + ':' + str(port)
sharesFile = baseDir + '/accounts/' + \
nickname + '@' + domain + '/shares.json'
if not os.path.isfile(sharesFile):
print('ERROR: no shares file ' + sharesFile)
return None
sharesJson = loadJson(sharesFile)
if not sharesJson:
print('ERROR: unable to load shares.json')
return None
if not sharesJson.get(itemID):
print('ERROR: share named "' + itemID + '" is not in ' + sharesFile)
return None
sharedItemDisplayName = sharesJson[itemID]['displayName']
sharedItemImageUrl = None
if sharesJson[itemID].get('imageUrl'):
sharedItemImageUrl = sharesJson[itemID]['imageUrl']
if os.path.isfile(baseDir + '/img/shares-background.png'):
if not os.path.isfile(baseDir + '/accounts/shares-background.png'):
copyfile(baseDir + '/img/shares-background.png',
baseDir + '/accounts/shares-background.png')
cssFilename = baseDir + '/epicyon-follow.css'
if os.path.isfile(baseDir + '/follow.css'):
cssFilename = baseDir + '/follow.css'
with open(cssFilename, 'r') as cssFile:
profileStyle = cssFile.read()
sharesStr = htmlHeader(cssFilename, profileStyle)
sharesStr += '\n'
sharesStr += '
\n'
sharesStr += '
\n'
if sharedItemImageUrl:
sharesStr += ' \n'
sharesStr += \
' ' + translate['Remove'] + \
' ' + sharedItemDisplayName + ' ?
\n'
postActor = getAltPath(actor, domainFull, callingDomain)
sharesStr += ' \n'
sharesStr += \
' \n'
sharesStr += ' \n'
sharesStr += \
' ' + \
translate['Yes'] + ' \n'
sharesStr += \
' ' + \
translate['No'] + ' \n'
sharesStr += ' \n'
sharesStr += ' \n'
sharesStr += '
\n'
sharesStr += '
\n'
sharesStr += htmlFooter()
return sharesStr
def htmlDeletePost(recentPostsCache: {}, maxRecentPosts: int,
translate, pageNumber: int,
session, baseDir: str, messageId: str,
httpPrefix: str, projectVersion: str,
wfRequest: {}, personCache: {},
callingDomain: str,
YTReplacementDomain: str) -> str:
"""Shows a screen asking to confirm the deletion of a post
"""
if '/statuses/' not in messageId:
return None
iconsDir = getIconsDir(baseDir)
actor = messageId.split('/statuses/')[0]
nickname = getNicknameFromActor(actor)
domain, port = getDomainFromActor(actor)
domainFull = domain
if port:
if port != 80 and port != 443:
domainFull = domain + ':' + str(port)
postFilename = locatePost(baseDir, nickname, domain, messageId)
if not postFilename:
return None
postJsonObject = loadJson(postFilename)
if not postJsonObject:
return None
if os.path.isfile(baseDir + '/img/delete-background.png'):
if not os.path.isfile(baseDir + '/accounts/delete-background.png'):
copyfile(baseDir + '/img/delete-background.png',
baseDir + '/accounts/delete-background.png')
deletePostStr = None
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
with open(cssFilename, 'r') as cssFile:
profileStyle = cssFile.read()
if httpPrefix != 'https':
profileStyle = profileStyle.replace('https://',
httpPrefix + '://')
deletePostStr = htmlHeader(cssFilename, profileStyle)
deletePostStr += \
individualPostAsHtml(True, recentPostsCache, maxRecentPosts,
iconsDir, translate, pageNumber,
baseDir, session, wfRequest, personCache,
nickname, domain, port, postJsonObject,
None, True, False,
httpPrefix, projectVersion, 'outbox',
YTReplacementDomain,
False, False, False, False, False)
deletePostStr += ''
deletePostStr += \
' ' + \
translate['Delete this post?'] + '
'
postActor = getAltPath(actor, domainFull, callingDomain)
deletePostStr += \
' \n'
deletePostStr += \
' \n'
deletePostStr += \
' \n'
deletePostStr += \
' ' + \
translate['Yes'] + ' \n'
deletePostStr += \
' ' + \
translate['No'] + ' \n'
deletePostStr += ' \n'
deletePostStr += ' \n'
deletePostStr += htmlFooter()
return deletePostStr
def htmlCalendarDeleteConfirm(translate: {}, baseDir: str,
path: str, httpPrefix: str,
domainFull: str, postId: str, postTime: str,
year: int, monthNumber: int,
dayNumber: int, callingDomain: str) -> str:
"""Shows a screen asking to confirm the deletion of a calendar event
"""
nickname = getNicknameFromActor(path)
actor = httpPrefix + '://' + domainFull + '/users/' + nickname
domain, port = getDomainFromActor(actor)
messageId = actor + '/statuses/' + postId
postFilename = locatePost(baseDir, nickname, domain, messageId)
if not postFilename:
return None
postJsonObject = loadJson(postFilename)
if not postJsonObject:
return None
if os.path.isfile(baseDir + '/img/delete-background.png'):
if not os.path.isfile(baseDir + '/accounts/delete-background.png'):
copyfile(baseDir + '/img/delete-background.png',
baseDir + '/accounts/delete-background.png')
deletePostStr = None
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
with open(cssFilename, 'r') as cssFile:
profileStyle = cssFile.read()
if httpPrefix != 'https':
profileStyle = profileStyle.replace('https://',
httpPrefix + '://')
deletePostStr = htmlHeader(cssFilename, profileStyle)
deletePostStr += \
'' + postTime + ' ' + str(year) + '/' + \
str(monthNumber) + \
'/' + str(dayNumber) + ' '
deletePostStr += ''
deletePostStr += ' ' + \
translate['Delete this event'] + '
'
postActor = getAltPath(actor, domainFull, callingDomain)
deletePostStr += \
' \n'
deletePostStr += ' \n'
deletePostStr += ' \n'
deletePostStr += ' \n'
deletePostStr += \
' \n'
deletePostStr += \
' \n'
deletePostStr += \
' ' + \
translate['Yes'] + ' \n'
deletePostStr += \
' ' + \
translate['No'] + ' \n'
deletePostStr += ' \n'
deletePostStr += ' \n'
deletePostStr += htmlFooter()
return deletePostStr
def htmlFollowConfirm(translate: {}, baseDir: str,
originPathStr: str,
followActor: str,
followProfileUrl: str) -> str:
"""Asks to confirm a follow
"""
followDomain, port = getDomainFromActor(followActor)
if os.path.isfile(baseDir + '/accounts/follow-background-custom.jpg'):
if not os.path.isfile(baseDir + '/accounts/follow-background.jpg'):
copyfile(baseDir + '/accounts/follow-background-custom.jpg',
baseDir + '/accounts/follow-background.jpg')
cssFilename = baseDir + '/epicyon-follow.css'
if os.path.isfile(baseDir + '/follow.css'):
cssFilename = baseDir + '/follow.css'
with open(cssFilename, 'r') as cssFile:
profileStyle = cssFile.read()
followStr = htmlHeader(cssFilename, profileStyle)
followStr += '\n'
followStr += '
\n'
followStr += '
\n'
followStr += ' \n'
followStr += ' \n'
followStr += \
' ' + translate['Follow'] + ' ' + \
getNicknameFromActor(followActor) + '@' + followDomain + ' ?
\n'
followStr += ' \n'
followStr += ' \n'
followStr += \
' ' + \
translate['Yes'] + ' \n'
followStr += \
' ' + \
translate['No'] + ' \n'
followStr += ' \n'
followStr += ' \n'
followStr += '
\n'
followStr += '
\n'
followStr += htmlFooter()
return followStr
def htmlUnfollowConfirm(translate: {}, baseDir: str,
originPathStr: str,
followActor: str,
followProfileUrl: str) -> str:
"""Asks to confirm unfollowing an actor
"""
followDomain, port = getDomainFromActor(followActor)
if os.path.isfile(baseDir + '/accounts/follow-background-custom.jpg'):
if not os.path.isfile(baseDir + '/accounts/follow-background.jpg'):
copyfile(baseDir + '/accounts/follow-background-custom.jpg',
baseDir + '/accounts/follow-background.jpg')
cssFilename = baseDir + '/epicyon-follow.css'
if os.path.isfile(baseDir + '/follow.css'):
cssFilename = baseDir + '/follow.css'
with open(cssFilename, 'r') as cssFile:
profileStyle = cssFile.read()
followStr = htmlHeader(cssFilename, profileStyle)
followStr += '\n'
followStr += '
\n'
followStr += '
\n'
followStr += ' \n'
followStr += ' \n'
followStr += \
' ' + translate['Stop following'] + \
' ' + getNicknameFromActor(followActor) + \
'@' + followDomain + ' ?
\n'
followStr += ' \n'
followStr += ' \n'
followStr += \
' ' + \
translate['Yes'] + ' \n'
followStr += \
' ' + \
translate['No'] + ' \n'
followStr += ' \n'
followStr += ' \n'
followStr += '
\n'
followStr += '
\n'
followStr += htmlFooter()
return followStr
def htmlPersonOptions(translate: {}, baseDir: str,
domain: 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'
with open(cssFilename, 'r') as cssFile:
profileStyle = cssFile.read()
profileStyle = \
profileStyle.replace('--follow-text-entry-width: 90%;',
'--follow-text-entry-width: 20%;')
# 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 = \
' ' + \
translate['Donate'] + ' \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'
if optionsNickname:
handle = optionsNickname + '@' + optionsDomainFull
petname = getPetName(baseDir, nickname, domain, handle)
optionsStr += \
' ' + translate['Petname'] + ': \n' + \
' \n' \
' ' + \
translate['Submit'] + ' \n'
if isFollowingActor(baseDir, nickname, domain, optionsActor):
if receivingCalendarEvents(baseDir, nickname, domain,
optionsNickname, optionsDomainFull):
optionsStr += \
' ' + \
translate['Receive calendar events from this account'] + \
'\n ' + \
translate['Submit'] + ' \n'
else:
optionsStr += \
' ' + \
translate['Receive calendar events from this account'] + \
'\n ' + \
translate['Submit'] + ' \n'
optionsStr += optionsLinkStr
optionsStr += \
' ' + translate['Go Back'] + ' \n'
optionsStr += \
' ' + \
translate['View'] + ' \n'
optionsStr += donateStr
optionsStr += \
' ' + translate[followStr] + ' \n'
optionsStr += \
' ' + translate[blockStr] + ' \n'
optionsStr += \
' ' + \
translate['DM'] + ' \n'
optionsStr += \
' ' + translate[snoozeButtonStr] + ' \n'
optionsStr += \
' ' + \
translate['Report'] + ' \n'
personNotes = ''
personNotesFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + \
'/notes/' + handle + '.txt'
if os.path.isfile(personNotesFilename):
with open(personNotesFilename, 'r') as fp:
personNotes = fp.read()
optionsStr += \
' ' + translate['Notes'] + ': \n'
optionsStr += ' ' + \
translate['Submit'] + ' \n'
optionsStr += \
' ' + \
personNotes + ' \n'
optionsStr += ' \n'
optionsStr += ' \n'
optionsStr += '
\n'
optionsStr += '
\n'
optionsStr += htmlFooter()
return optionsStr
def htmlUnblockConfirm(translate: {}, baseDir: str,
originPathStr: str,
blockActor: str,
blockProfileUrl: str) -> str:
"""Asks to confirm unblocking an actor
"""
blockDomain, port = getDomainFromActor(blockActor)
if os.path.isfile(baseDir + '/img/block-background.png'):
if not os.path.isfile(baseDir + '/accounts/block-background.png'):
copyfile(baseDir + '/img/block-background.png',
baseDir + '/accounts/block-background.png')
cssFilename = baseDir + '/epicyon-follow.css'
if os.path.isfile(baseDir + '/follow.css'):
cssFilename = baseDir + '/follow.css'
with open(cssFilename, 'r') as cssFile:
profileStyle = cssFile.read()
blockStr = htmlHeader(cssFilename, profileStyle)
blockStr += '\n'
blockStr += '
\n'
blockStr += '
\n'
blockStr += ' \n'
blockStr += ' \n'
blockStr += \
' ' + translate['Stop blocking'] + ' ' + \
getNicknameFromActor(blockActor) + '@' + blockDomain + ' ?
\n'
blockStr += ' \n'
blockStr += ' \n'
blockStr += \
' ' + \
translate['Yes'] + ' \n'
blockStr += \
' ' + \
translate['No'] + ' \n'
blockStr += ' \n'
blockStr += ' \n'
blockStr += '
\n'
blockStr += '
\n'
blockStr += htmlFooter()
return blockStr
def htmlSearchEmojiTextEntry(translate: {},
baseDir: str, path: str) -> str:
"""Search for an emoji by name
"""
# emoji.json is generated so that it can be customized and the changes
# will be retained even if default_emoji.json is subsequently updated
if not os.path.isfile(baseDir + '/emoji/emoji.json'):
copyfile(baseDir + '/emoji/default_emoji.json',
baseDir + '/emoji/emoji.json')
actor = path.replace('/search', '')
domain, port = getDomainFromActor(actor)
if os.path.isfile(baseDir + '/img/search-background.png'):
if not os.path.isfile(baseDir + '/accounts/search-background.png'):
copyfile(baseDir + '/img/search-background.png',
baseDir + '/accounts/search-background.png')
cssFilename = baseDir + '/epicyon-follow.css'
if os.path.isfile(baseDir + '/follow.css'):
cssFilename = baseDir + '/follow.css'
with open(cssFilename, 'r') as cssFile:
profileStyle = cssFile.read()
emojiStr = htmlHeader(cssFilename, profileStyle)
emojiStr += '\n'
emojiStr += '
\n'
emojiStr += '
\n'
emojiStr += \
' ' + \
translate['Enter an emoji name to search for'] + '
\n'
emojiStr += ' \n'
emojiStr += ' \n'
emojiStr += ' \n'
emojiStr += \
' ' + \
translate['Submit'] + ' \n'
emojiStr += ' \n'
emojiStr += ' \n'
emojiStr += '
\n'
emojiStr += '
\n'
emojiStr += htmlFooter()
return emojiStr
def weekDayOfMonthStart(monthNumber: int, year: int) -> int:
"""Gets the day number of the first day of the month
1=sun, 7=sat
"""
firstDayOfMonth = datetime(year, monthNumber, 1, 0, 0)
return int(firstDayOfMonth.strftime("%w")) + 1
def htmlCalendarDay(translate: {},
baseDir: str, path: str,
year: int, monthNumber: int, dayNumber: int,
nickname: str, domain: str, dayEvents: [],
monthName: str, actor: str) -> str:
"""Show a day within the calendar
"""
accountDir = baseDir + '/accounts/' + nickname + '@' + domain
calendarFile = accountDir + '/.newCalendar'
if os.path.isfile(calendarFile):
os.remove(calendarFile)
cssFilename = baseDir + '/epicyon-calendar.css'
if os.path.isfile(baseDir + '/calendar.css'):
cssFilename = baseDir + '/calendar.css'
with open(cssFilename, 'r') as cssFile:
calendarStyle = cssFile.read()
calActor = actor
if '/users/' in actor:
calActor = '/users/' + actor.split('/users/')[1]
calendarStr = htmlHeader(cssFilename, calendarStyle)
calendarStr += '\n'
calendarStr += '\n'
calendarStr += \
' \n'
calendarStr += \
' ' + str(dayNumber) + ' ' + monthName + \
' ' + str(year) + ' \n'
calendarStr += ' \n'
calendarStr += '\n'
iconsDir = getIconsDir(baseDir)
if dayEvents:
for eventPost in dayEvents:
eventTime = None
eventDescription = None
eventPlace = None
postId = None
# get the time place and description
for ev in eventPost:
if ev['type'] == 'Event':
if ev.get('postId'):
postId = ev['postId']
if ev.get('startTime'):
eventDate = \
datetime.strptime(ev['startTime'],
"%Y-%m-%dT%H:%M:%S%z")
eventTime = eventDate.strftime("%H:%M").strip()
if ev.get('name'):
eventDescription = ev['name'].strip()
elif ev['type'] == 'Place':
if ev.get('name'):
eventPlace = ev['name']
deleteButtonStr = ''
if postId:
deleteButtonStr = \
'\n \n'
if eventTime and eventDescription and eventPlace:
calendarStr += \
'' + eventTime + \
' ' + \
'' + \
eventPlace + ' ' + eventDescription + \
' ' + deleteButtonStr + ' \n'
elif eventTime and eventDescription and not eventPlace:
calendarStr += \
'' + eventTime + \
' ' + \
eventDescription + ' ' + deleteButtonStr + ' \n'
elif not eventTime and eventDescription and not eventPlace:
calendarStr += \
'' + \
' ' + \
eventDescription + ' ' + deleteButtonStr + ' \n'
elif not eventTime and eventDescription and eventPlace:
calendarStr += \
' ' + \
'' + \
eventPlace + ' ' + eventDescription + \
' ' + deleteButtonStr + ' \n'
elif eventTime and not eventDescription and eventPlace:
calendarStr += \
'' + eventTime + \
' ' + \
'' + \
eventPlace + ' ' + \
deleteButtonStr + ' \n'
calendarStr += ' \n'
calendarStr += '
\n'
calendarStr += htmlFooter()
return calendarStr
def htmlCalendar(translate: {},
baseDir: str, path: str,
httpPrefix: str, domainFull: str) -> str:
"""Show the calendar for a person
"""
iconsDir = getIconsDir(baseDir)
domain = domainFull
if ':' in domainFull:
domain = domainFull.split(':')[0]
monthNumber = 0
dayNumber = None
year = 1970
actor = httpPrefix + '://' + domainFull + path.replace('/calendar', '')
if '?' in actor:
first = True
for p in actor.split('?'):
if not first:
if '=' in p:
if p.split('=')[0] == 'year':
numStr = p.split('=')[1]
if numStr.isdigit():
year = int(numStr)
elif p.split('=')[0] == 'month':
numStr = p.split('=')[1]
if numStr.isdigit():
monthNumber = int(numStr)
elif p.split('=')[0] == 'day':
numStr = p.split('=')[1]
if numStr.isdigit():
dayNumber = int(numStr)
first = False
actor = actor.split('?')[0]
currDate = datetime.now()
if year == 1970 and monthNumber == 0:
year = currDate.year
monthNumber = currDate.month
nickname = getNicknameFromActor(actor)
if os.path.isfile(baseDir + '/img/calendar-background.png'):
if not os.path.isfile(baseDir + '/accounts/calendar-background.png'):
copyfile(baseDir + '/img/calendar-background.png',
baseDir + '/accounts/calendar-background.png')
months = ('January', 'February', 'March', 'April',
'May', 'June', 'July', 'August', 'September',
'October', 'November', 'December')
monthName = translate[months[monthNumber - 1]]
if dayNumber:
dayEvents = None
events = \
getTodaysEvents(baseDir, nickname, domain,
year, monthNumber, dayNumber)
if events:
if events.get(str(dayNumber)):
dayEvents = events[str(dayNumber)]
return htmlCalendarDay(translate, baseDir, path,
year, monthNumber, dayNumber,
nickname, domain, dayEvents,
monthName, actor)
events = \
getCalendarEvents(baseDir, nickname, domain, year, monthNumber)
prevYear = year
prevMonthNumber = monthNumber - 1
if prevMonthNumber < 1:
prevMonthNumber = 12
prevYear = year - 1
nextYear = year
nextMonthNumber = monthNumber + 1
if nextMonthNumber > 12:
nextMonthNumber = 1
nextYear = year + 1
print('Calendar year=' + str(year) + ' month=' + str(monthNumber) +
' ' + str(weekDayOfMonthStart(monthNumber, year)))
if monthNumber < 12:
daysInMonth = \
(date(year, monthNumber + 1, 1) - date(year, monthNumber, 1)).days
else:
daysInMonth = \
(date(year + 1, 1, 1) - date(year, monthNumber, 1)).days
# print('daysInMonth ' + str(monthNumber) + ': ' + str(daysInMonth))
cssFilename = baseDir + '/epicyon-calendar.css'
if os.path.isfile(baseDir + '/calendar.css'):
cssFilename = baseDir + '/calendar.css'
with open(cssFilename, 'r') as cssFile:
calendarStyle = cssFile.read()
calActor = actor
if '/users/' in actor:
calActor = '/users/' + actor.split('/users/')[1]
calendarStr = htmlHeader(cssFilename, calendarStyle)
calendarStr += '\n'
calendarStr += '\n'
calendarStr += \
' '
calendarStr += \
' \n'
calendarStr += ' '
calendarStr += ' ' + monthName + ' \n'
calendarStr += \
' '
calendarStr += \
' \n'
calendarStr += ' \n'
calendarStr += '\n'
calendarStr += '\n'
calendarStr += ' \n'
calendarStr += ' \n'
calendarStr += ' \n'
calendarStr += ' \n'
calendarStr += ' \n'
calendarStr += ' \n'
calendarStr += ' \n'
calendarStr += ' \n'
calendarStr += ' \n'
calendarStr += '\n'
dayOfMonth = 0
dow = weekDayOfMonthStart(monthNumber, year)
for weekOfMonth in range(1, 7):
if dayOfMonth == daysInMonth:
continue
calendarStr += ' \n'
for dayNumber in range(1, 8):
if (weekOfMonth > 1 and dayOfMonth < daysInMonth) or \
(weekOfMonth == 1 and dayNumber >= dow):
dayOfMonth += 1
isToday = False
if year == currDate.year:
if currDate.month == monthNumber:
if dayOfMonth == currDate.day:
isToday = True
if events.get(str(dayOfMonth)):
url = calActor + '/calendar?year=' + \
str(year) + '?month=' + \
str(monthNumber) + '?day=' + str(dayOfMonth)
dayLink = '' + \
str(dayOfMonth) + ' '
# there are events for this day
if not isToday:
calendarStr += \
' ' + \
dayLink + ' \n'
else:
calendarStr += \
' ' + \
dayLink + ' \n'
else:
# No events today
if not isToday:
calendarStr += \
' ' + \
str(dayOfMonth) + ' \n'
else:
calendarStr += \
' ' + str(dayOfMonth) + ' \n'
else:
calendarStr += ' \n'
calendarStr += ' \n'
calendarStr += ' \n'
calendarStr += '
\n'
calendarStr += htmlFooter()
return calendarStr
def removeOldHashtags(baseDir: str, maxMonths: int) -> str:
"""Remove old hashtags
"""
if maxMonths > 11:
maxMonths = 11
maxDaysSinceEpoch = \
(datetime.utcnow() - datetime(1970, 1 + maxMonths, 1)).days
removeHashtags = []
for subdir, dirs, files in os.walk(baseDir + '/tags'):
for f in files:
tagsFilename = os.path.join(baseDir + '/tags', f)
if not os.path.isfile(tagsFilename):
continue
# get last modified datetime
modTimesinceEpoc = os.path.getmtime(tagsFilename)
lastModifiedDate = datetime.fromtimestamp(modTimesinceEpoc)
fileDaysSinceEpoch = (lastModifiedDate - datetime(1970, 1, 1)).days
# check of the file is too old
if fileDaysSinceEpoch < maxDaysSinceEpoch:
removeHashtags.append(tagsFilename)
for removeFilename in removeHashtags:
try:
os.remove(removeFilename)
except BaseException:
pass
def htmlHashTagSwarm(baseDir: str, actor: str) -> str:
"""Returns a tag swarm of today's hashtags
"""
currTime = datetime.utcnow()
daysSinceEpoch = (currTime - datetime(1970, 1, 1)).days
daysSinceEpochStr = str(daysSinceEpoch) + ' '
tagSwarm = []
for subdir, dirs, files in os.walk(baseDir + '/tags'):
for f in files:
tagsFilename = os.path.join(baseDir + '/tags', f)
if not os.path.isfile(tagsFilename):
continue
# get last modified datetime
modTimesinceEpoc = os.path.getmtime(tagsFilename)
lastModifiedDate = datetime.fromtimestamp(modTimesinceEpoc)
fileDaysSinceEpoch = (lastModifiedDate - datetime(1970, 1, 1)).days
# check if the file was last modified today
if fileDaysSinceEpoch != daysSinceEpoch:
continue
hashTagName = f.split('.')[0]
if isBlockedHashtag(baseDir, hashTagName):
continue
if daysSinceEpochStr not in open(tagsFilename).read():
continue
with open(tagsFilename, 'r') as tagsFile:
line = tagsFile.readline()
lineCtr = 1
tagCtr = 0
maxLineCtr = 1
while line:
if ' ' not in line:
line = tagsFile.readline()
lineCtr += 1
# don't read too many lines
if lineCtr >= maxLineCtr:
break
continue
postDaysSinceEpochStr = line.split(' ')[0]
if not postDaysSinceEpochStr.isdigit():
line = tagsFile.readline()
lineCtr += 1
# don't read too many lines
if lineCtr >= maxLineCtr:
break
continue
postDaysSinceEpoch = int(postDaysSinceEpochStr)
if postDaysSinceEpoch < daysSinceEpoch:
break
if postDaysSinceEpoch == daysSinceEpoch:
if tagCtr == 0:
tagSwarm.append(hashTagName)
tagCtr += 1
line = tagsFile.readline()
lineCtr += 1
# don't read too many lines
if lineCtr >= maxLineCtr:
break
if not tagSwarm:
return ''
tagSwarm.sort()
tagSwarmStr = ''
ctr = 0
for tagName in tagSwarm:
tagSwarmStr += \
'' + tagName + ' \n'
ctr += 1
tagSwarmHtml = tagSwarmStr.strip() + '\n'
return tagSwarmHtml
def htmlSearch(translate: {},
baseDir: str, path: str, domain: str) -> str:
"""Search called from the timeline icon
"""
actor = path.replace('/search', '')
searchNickname = getNicknameFromActor(actor)
if os.path.isfile(baseDir + '/img/search-background.png'):
if not os.path.isfile(baseDir + '/accounts/search-background.png'):
copyfile(baseDir + '/img/search-background.png',
baseDir + '/accounts/search-background.png')
cssFilename = baseDir + '/epicyon-search.css'
if os.path.isfile(baseDir + '/search.css'):
cssFilename = baseDir + '/search.css'
with open(cssFilename, 'r') as cssFile:
profileStyle = cssFile.read()
followStr = htmlHeader(cssFilename, profileStyle)
# show a banner above the search box
searchBannerFilename = \
baseDir + '/accounts/' + searchNickname + '@' + domain + \
'/search_banner.png'
if not os.path.isfile(searchBannerFilename):
theme = getConfigParam(baseDir, 'theme').lower()
if theme == 'default':
theme = ''
else:
theme = '_' + theme
themeSearchBannerFilename = \
baseDir + '/img/search_banner' + theme + '.png'
if os.path.isfile(themeSearchBannerFilename):
copyfile(themeSearchBannerFilename, searchBannerFilename)
if os.path.isfile(searchBannerFilename):
followStr += '\n\n' + \
' ' + \
' \n
\n \n'
# show the search box
followStr += '\n'
followStr += '
\n'
followStr += '
\n'
idx = 'Enter an address, shared item, !history, #hashtag, ' + \
'*skill or :emoji: to search for'
followStr += \
' ' + translate[idx] + '
\n'
followStr += ' \n'
followStr += \
' \n'
followStr += ' \n'
followStr += ' ' + translate['Go Back'] + ' \n'
followStr += ' ' + translate['Submit'] + ' \n'
followStr += ' \n'
followStr += ' ' + \
htmlHashTagSwarm(baseDir, actor) + '
\n'
followStr += ' \n'
followStr += '
\n'
followStr += '
\n'
followStr += htmlFooter()
return followStr
def htmlProfileAfterSearch(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) -> 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'
with open(cssFilename, 'r') as cssFile:
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 = cssFile.read().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'
profileStr += \
' \n'
profileStr += \
' ' + \
translate['Go Back'] + ' \n'
profileStr += \
' ' + \
translate['Follow'] + ' \n'
profileStr += \
' ' + \
translate['View'] + ' \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,
False, False, False, False, False)
i += 1
if i >= 20:
break
return htmlHeader(cssFilename, profileStyle) + profileStr + htmlFooter()