__filename__ = "webapp.py" __author__ = "Bob Mottram" __license__ = "AGPL3+" __version__ = "1.1.0" __maintainer__ = "Bob Mottram" __email__ = "bob@freedombone.net" __status__ = "Production" import time import os from datetime import datetime from shutil import copyfile from pprint import pprint from person import personBoxJson from person import isPersonSnoozed from pgp import getEmailAddress from pgp import getPGPpubKey from pgp import getPGPfingerprint from xmpp import getXmppAddress from ssb import getSSBAddress from tox import getToxAddress from matrix import getMatrixAddress from donate import getDonationUrl from utils import getCSS from utils import isSystemAccount from utils import removeIdEnding from utils import getNicknameFromActor from utils import getDomainFromActor from utils import locatePost from utils import noOfAccounts from utils import isPublicPostFromUrl from utils import loadJson from utils import getConfigParam from utils import votesOnNewswireItem from utils import removeHtml from follow import isFollowingActor from follow import followerApprovalActive from webfinger import webfingerHandle from posts import getPersonBox from posts import getUserUrl from posts import parseUserFeed from posts import populateRepliesJson from posts import isModerator from posts import isEditor from session import getJson from blocking import isBlocked from content import removeLongWords from skills import getSkills from shares import getValidSharedItemID from happening import todaysEventsCheck from happening import thisWeeksEventsCheck from theme import getThemesList from petnames import getPetName from followingCalendar import receivingCalendarEvents from webapp_utils import getAltPath from webapp_utils import getBlogAddress from webapp_utils import getPersonAvatarUrl from webapp_utils import getIconsDir from webapp_utils import scheduledPostsExist from webapp_utils import sharesTimelineJson from webapp_utils import getImageFile from webapp_utils import getBannerFile from webapp_utils import getLeftImageFile from webapp_utils import getRightImageFile from webapp_utils import htmlHeader from webapp_utils import htmlFooter from webapp_utils import addEmojiToDisplayName from webapp_utils import htmlPostSeparator from webapp_post import individualPostAsHtml from webapp_post import preparePostFromHtmlCache def htmlFollowingList(cssCache: {}, baseDir: str, followingFilename: str) -> str: """Returns a list of handles being followed """ with open(followingFilename, 'r') as followingFile: msg = followingFile.read() followingList = msg.split('\n') followingList.sort() if followingList: cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' profileCSS = getCSS(baseDir, cssFilename, cssCache) if profileCSS: followingListHtml = htmlHeader(cssFilename, profileCSS) for followingAddress in followingList: if followingAddress: followingListHtml += \ '

@' + followingAddress + '

' followingListHtml += htmlFooter() msg = followingListHtml return msg return '' def htmlFollowingDataList(baseDir: str, nickname: str, domain: str, domainFull: str) -> str: """Returns a datalist of handles being followed """ listStr = '\n' followingFilename = \ baseDir + '/accounts/' + nickname + '@' + domain + '/following.txt' if os.path.isfile(followingFilename): with open(followingFilename, 'r') as followingFile: msg = followingFile.read() # add your own handle, so that you can send DMs # to yourself as reminders msg += nickname + '@' + domainFull + '\n' # include petnames petnamesFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/petnames.txt' if os.path.isfile(petnamesFilename): followingList = [] with open(petnamesFilename, 'r') as petnamesFile: petStr = petnamesFile.read() # extract each petname and append it petnamesList = petStr.split('\n') for pet in petnamesList: followingList.append(pet.split(' ')[0]) # add the following.txt entries followingList += msg.split('\n') else: # no petnames list exists - just use following.txt followingList = msg.split('\n') followingList.sort() if followingList: for followingAddress in followingList: if followingAddress: listStr += \ '\n' listStr += '\n' return listStr def htmlModerationInfo(cssCache: {}, translate: {}, baseDir: str, httpPrefix: str) -> str: msgStr1 = \ 'These are globally blocked for all accounts on this instance' msgStr2 = \ 'Any blocks or suspensions made by moderators will be shown here.' infoForm = '' cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' infoCSS = getCSS(baseDir, cssFilename, cssCache) if infoCSS: if httpPrefix != 'https': infoCSS = infoCSS.replace('https://', httpPrefix + '://') infoForm = htmlHeader(cssFilename, infoCSS) infoForm += \ '

' + \ translate['Moderation Information'] + \ '

' infoShown = False suspendedFilename = baseDir + '/accounts/suspended.txt' if os.path.isfile(suspendedFilename): with open(suspendedFilename, "r") as f: suspendedStr = f.read() infoForm += '
' infoForm += '
' + \ translate['Suspended accounts'] + '' infoForm += '
' + \ translate['These are currently suspended'] infoForm += \ ' ' infoForm += '
' infoShown = True blockingFilename = baseDir + '/accounts/blocking.txt' if os.path.isfile(blockingFilename): with open(blockingFilename, "r") as f: blockedStr = f.read() infoForm += '
' infoForm += \ '
' + \ translate['Blocked accounts and hashtags'] + '' infoForm += \ '
' + \ translate[msgStr1] infoForm += \ ' ' infoForm += '
' infoShown = True if not infoShown: infoForm += \ '

' + \ translate[msgStr2] + \ '

' infoForm += htmlFooter() return infoForm def htmlEditLinks(cssCache: {}, translate: {}, baseDir: str, path: str, domain: str, port: int, httpPrefix: str, defaultTimeline: str) -> str: """Shows the edit links screen """ if '/users/' not in path: return '' path = path.replace('/inbox', '').replace('/outbox', '') path = path.replace('/shares', '') nickname = getNicknameFromActor(path) if not nickname: return '' # is the user a moderator? if not isEditor(baseDir, nickname): return '' cssFilename = baseDir + '/epicyon-links.css' if os.path.isfile(baseDir + '/links.css'): cssFilename = baseDir + '/links.css' editCSS = getCSS(baseDir, cssFilename, cssCache) if editCSS: if httpPrefix != 'https': editCSS = \ editCSS.replace('https://', httpPrefix + '://') # filename of the banner shown at the top bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) editLinksForm = htmlHeader(cssFilename, editCSS) # top banner editLinksForm += \ '\n' editLinksForm += '\n' editLinksForm += \ '
\n' editLinksForm += \ '
\n' editLinksForm += \ '

' + translate['Edit Links'] + '

' editLinksForm += \ '
\n' # editLinksForm += \ # ' \n' editLinksForm += \ '
\n' + \ ' \n' + \ '
\n' editLinksForm += \ '
\n' linksFilename = baseDir + '/accounts/links.txt' linksStr = '' if os.path.isfile(linksFilename): with open(linksFilename, 'r') as fp: linksStr = fp.read() editLinksForm += \ '
' editLinksForm += \ ' ' + \ translate['One link per line. Description followed by the link.'] + \ '
' editLinksForm += \ ' ' editLinksForm += \ '
' editLinksForm += htmlFooter() return editLinksForm def htmlEditNewswire(cssCache: {}, translate: {}, baseDir: str, path: str, domain: str, port: int, httpPrefix: str, defaultTimeline: str) -> str: """Shows the edit newswire screen """ if '/users/' not in path: return '' path = path.replace('/inbox', '').replace('/outbox', '') path = path.replace('/shares', '') nickname = getNicknameFromActor(path) if not nickname: return '' # is the user a moderator? if not isModerator(baseDir, nickname): return '' cssFilename = baseDir + '/epicyon-links.css' if os.path.isfile(baseDir + '/links.css'): cssFilename = baseDir + '/links.css' editCSS = getCSS(baseDir, cssFilename, cssCache) if editCSS: if httpPrefix != 'https': editCSS = \ editCSS.replace('https://', httpPrefix + '://') # filename of the banner shown at the top bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) editNewswireForm = htmlHeader(cssFilename, editCSS) # top banner editNewswireForm += \ '\n' editNewswireForm += '\n' editNewswireForm += \ '\n' editNewswireForm += \ '
\n' editNewswireForm += \ '

' + translate['Edit newswire'] + '

' editNewswireForm += \ '
\n' # editNewswireForm += \ # ' \n' editNewswireForm += \ '
\n' + \ ' \n' + \ '
\n' editNewswireForm += \ '
\n' newswireFilename = baseDir + '/accounts/newswire.txt' newswireStr = '' if os.path.isfile(newswireFilename): with open(newswireFilename, 'r') as fp: newswireStr = fp.read() editNewswireForm += \ '
' editNewswireForm += \ ' ' + \ translate['Add RSS feed links below.'] + \ '
' editNewswireForm += \ ' ' filterStr = '' filterFilename = \ baseDir + '/accounts/news@' + domain + '/filters.txt' if os.path.isfile(filterFilename): with open(filterFilename, 'r') as filterfile: filterStr = filterfile.read() editNewswireForm += \ '
\n' editNewswireForm += '
' editNewswireForm += ' \n' hashtagRulesStr = '' hashtagRulesFilename = \ baseDir + '/accounts/hashtagrules.txt' if os.path.isfile(hashtagRulesFilename): with open(hashtagRulesFilename, 'r') as rulesfile: hashtagRulesStr = rulesfile.read() editNewswireForm += \ '
\n' editNewswireForm += '
\n' editNewswireForm += \ ' ' + translate['See instructions'] + '\n' editNewswireForm += ' \n' editNewswireForm += \ '
' editNewswireForm += htmlFooter() return editNewswireForm def htmlEditNewsPost(cssCache: {}, translate: {}, baseDir: str, path: str, domain: str, port: int, httpPrefix: str, postUrl: str) -> str: """Edits a news post """ if '/users/' not in path: return '' pathOriginal = path nickname = getNicknameFromActor(path) if not nickname: return '' # is the user an editor? if not isEditor(baseDir, nickname): return '' postUrl = postUrl.replace('/', '#') postFilename = locatePost(baseDir, nickname, domain, postUrl) if not postFilename: return '' postJsonObject = loadJson(postFilename) if not postJsonObject: return '' cssFilename = baseDir + '/epicyon-links.css' if os.path.isfile(baseDir + '/links.css'): cssFilename = baseDir + '/links.css' editCSS = getCSS(baseDir, cssFilename, cssCache) if editCSS: if httpPrefix != 'https': editCSS = \ editCSS.replace('https://', httpPrefix + '://') editNewsPostForm = htmlHeader(cssFilename, editCSS) editNewsPostForm += \ '\n' editNewsPostForm += \ '
\n' editNewsPostForm += \ '

' + translate['Edit News Post'] + '

' editNewsPostForm += \ '
\n' editNewsPostForm += \ ' ' + \ '\n' editNewsPostForm += \ ' \n' editNewsPostForm += \ '
\n' editNewsPostForm += \ '
' editNewsPostForm += \ ' \n' newsPostTitle = postJsonObject['object']['summary'] editNewsPostForm += \ '
\n' newsPostContent = postJsonObject['object']['content'] editNewsPostForm += \ ' ' editNewsPostForm += \ '
' editNewsPostForm += htmlFooter() return editNewsPostForm def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str, domain: str, port: int, httpPrefix: str, defaultTimeline: str) -> str: """Shows the edit profile screen """ imageFormats = '.png, .jpg, .jpeg, .gif, .webp, .avif' path = path.replace('/inbox', '').replace('/outbox', '') path = path.replace('/shares', '') nickname = getNicknameFromActor(path) if not nickname: return '' domainFull = domain if port: if port != 80 and port != 443: if ':' not in domain: domainFull = domain + ':' + str(port) actorFilename = \ baseDir + '/accounts/' + nickname + '@' + domain + '.json' if not os.path.isfile(actorFilename): return '' # filename of the banner shown at the top bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) isBot = '' isGroup = '' followDMs = '' removeTwitter = '' notifyLikes = '' hideLikeButton = '' mediaInstanceStr = '' blogsInstanceStr = '' newsInstanceStr = '' displayNickname = nickname bioStr = '' donateUrl = '' emailAddress = '' PGPpubKey = '' PGPfingerprint = '' xmppAddress = '' matrixAddress = '' ssbAddress = '' blogAddress = '' toxAddress = '' manuallyApprovesFollowers = '' actorJson = loadJson(actorFilename) if actorJson: donateUrl = getDonationUrl(actorJson) xmppAddress = getXmppAddress(actorJson) matrixAddress = getMatrixAddress(actorJson) ssbAddress = getSSBAddress(actorJson) blogAddress = getBlogAddress(actorJson) toxAddress = getToxAddress(actorJson) emailAddress = getEmailAddress(actorJson) PGPpubKey = getPGPpubKey(actorJson) PGPfingerprint = getPGPfingerprint(actorJson) if actorJson.get('name'): displayNickname = actorJson['name'] if actorJson.get('summary'): bioStr = \ actorJson['summary'].replace('

', '').replace('

', '') if actorJson.get('manuallyApprovesFollowers'): if actorJson['manuallyApprovesFollowers']: manuallyApprovesFollowers = 'checked' else: manuallyApprovesFollowers = '' if actorJson.get('type'): if actorJson['type'] == 'Service': isBot = 'checked' isGroup = '' elif actorJson['type'] == 'Group': isGroup = 'checked' isBot = '' if os.path.isfile(baseDir + '/accounts/' + nickname + '@' + domain + '/.followDMs'): followDMs = 'checked' if os.path.isfile(baseDir + '/accounts/' + nickname + '@' + domain + '/.removeTwitter'): removeTwitter = 'checked' if os.path.isfile(baseDir + '/accounts/' + nickname + '@' + domain + '/.notifyLikes'): notifyLikes = 'checked' if os.path.isfile(baseDir + '/accounts/' + nickname + '@' + domain + '/.hideLikeButton'): hideLikeButton = 'checked' mediaInstance = getConfigParam(baseDir, "mediaInstance") if mediaInstance: if mediaInstance is True: mediaInstanceStr = 'checked' blogsInstanceStr = '' newsInstanceStr = '' newsInstance = getConfigParam(baseDir, "newsInstance") if newsInstance: if newsInstance is True: newsInstanceStr = 'checked' blogsInstanceStr = '' mediaInstanceStr = '' blogsInstance = getConfigParam(baseDir, "blogsInstance") if blogsInstance: if blogsInstance is True: blogsInstanceStr = 'checked' mediaInstanceStr = '' newsInstanceStr = '' filterStr = '' filterFilename = \ baseDir + '/accounts/' + nickname + '@' + domain + '/filters.txt' if os.path.isfile(filterFilename): with open(filterFilename, 'r') as filterfile: filterStr = filterfile.read() switchStr = '' switchFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/replacewords.txt' if os.path.isfile(switchFilename): with open(switchFilename, 'r') as switchfile: switchStr = switchfile.read() autoTags = '' autoTagsFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/autotags.txt' if os.path.isfile(autoTagsFilename): with open(autoTagsFilename, 'r') as autoTagsFile: autoTags = autoTagsFile.read() autoCW = '' autoCWFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/autocw.txt' if os.path.isfile(autoCWFilename): with open(autoCWFilename, 'r') as autoCWFile: autoCW = autoCWFile.read() blockedStr = '' blockedFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/blocking.txt' if os.path.isfile(blockedFilename): with open(blockedFilename, 'r') as blockedfile: blockedStr = blockedfile.read() allowedInstancesStr = '' allowedInstancesFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/allowedinstances.txt' if os.path.isfile(allowedInstancesFilename): with open(allowedInstancesFilename, 'r') as allowedInstancesFile: allowedInstancesStr = allowedInstancesFile.read() gitProjectsStr = '' gitProjectsFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/gitprojects.txt' if os.path.isfile(gitProjectsFilename): with open(gitProjectsFilename, 'r') as gitProjectsFile: gitProjectsStr = gitProjectsFile.read() skills = getSkills(baseDir, nickname, domain) skillsStr = '' skillCtr = 1 if skills: for skillDesc, skillValue in skills.items(): skillsStr += \ '

' skillsStr += \ '

' skillCtr += 1 skillsStr += \ '

' skillsStr += \ '

' cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' editProfileCSS = getCSS(baseDir, cssFilename, cssCache) if editProfileCSS: if httpPrefix != 'https': editProfileCSS = \ editProfileCSS.replace('https://', httpPrefix + '://') moderatorsStr = '' themesDropdown = '' instanceStr = '' adminNickname = getConfigParam(baseDir, 'admin') if adminNickname: if path.startswith('/users/' + adminNickname + '/'): instanceDescription = \ getConfigParam(baseDir, 'instanceDescription') instanceDescriptionShort = \ getConfigParam(baseDir, 'instanceDescriptionShort') instanceTitle = \ getConfigParam(baseDir, 'instanceTitle') instanceStr += '
' instanceStr += \ ' ' if instanceTitle: instanceStr += \ '
' else: instanceStr += \ '
' instanceStr += \ ' ' if instanceDescriptionShort: instanceStr += \ '
' else: instanceStr += \ '
' instanceStr += \ ' ' if instanceDescription: instanceStr += \ ' ' else: instanceStr += \ ' ' instanceStr += \ ' ' instanceStr += \ ' ' instanceStr += '
' moderators = '' moderatorsFile = baseDir + '/accounts/moderators.txt' if os.path.isfile(moderatorsFile): with open(moderatorsFile, "r") as f: moderators = f.read() moderatorsStr = '
' moderatorsStr += ' ' + translate['Moderators'] + '
' moderatorsStr += ' ' + \ translate['A list of moderator nicknames. One per line.'] moderatorsStr += \ ' ' moderatorsStr += '
' editors = '' editorsFile = baseDir + '/accounts/editors.txt' if os.path.isfile(editorsFile): with open(editorsFile, "r") as f: editors = f.read() editorsStr = '
' editorsStr += ' ' + translate['Site Editors'] + '
' editorsStr += ' ' + \ translate['A list of editor nicknames. One per line.'] editorsStr += \ ' ' editorsStr += '
' themes = getThemesList() themesDropdown = '
' themesDropdown += ' ' + translate['Theme'] + '
' grayscaleFilename = \ baseDir + '/accounts/.grayscale' grayscale = '' if os.path.isfile(grayscaleFilename): grayscale = 'checked' themesDropdown += \ ' ' + translate['Grayscale'] + '
' themesDropdown += '
' if os.path.isfile(baseDir + '/fonts/custom.woff') or \ os.path.isfile(baseDir + '/fonts/custom.woff2') or \ os.path.isfile(baseDir + '/fonts/custom.otf') or \ os.path.isfile(baseDir + '/fonts/custom.ttf'): themesDropdown += \ ' ' + \ translate['Remove the custom font'] + '
' themesDropdown += '
' themeName = getConfigParam(baseDir, 'theme') themesDropdown = \ themesDropdown.replace('\n' editProfileForm += htmlFooter() return editProfileForm def htmlGetLoginCredentials(loginParams: str, lastLoginTime: int) -> (str, str, bool): """Receives login credentials via HTTPServer POST """ if not loginParams.startswith('username='): return None, None, None # minimum time between login attempts currTime = int(time.time()) if currTime < lastLoginTime+10: return None, None, None if '&' not in loginParams: return None, None, None loginArgs = loginParams.split('&') nickname = None password = None register = False for arg in loginArgs: if '=' in arg: if arg.split('=', 1)[0] == 'username': nickname = arg.split('=', 1)[1] elif arg.split('=', 1)[0] == 'password': password = arg.split('=', 1)[1] elif arg.split('=', 1)[0] == 'register': register = True return nickname, password, register def htmlLogin(cssCache: {}, translate: {}, baseDir: str, autocomplete=True) -> str: """Shows the login screen """ accounts = noOfAccounts(baseDir) loginImage = 'login.png' loginImageFilename = None if os.path.isfile(baseDir + '/accounts/' + loginImage): loginImageFilename = baseDir + '/accounts/' + loginImage elif os.path.isfile(baseDir + '/accounts/login.jpg'): loginImage = 'login.jpg' loginImageFilename = baseDir + '/accounts/' + loginImage elif os.path.isfile(baseDir + '/accounts/login.jpeg'): loginImage = 'login.jpeg' loginImageFilename = baseDir + '/accounts/' + loginImage elif os.path.isfile(baseDir + '/accounts/login.gif'): loginImage = 'login.gif' loginImageFilename = baseDir + '/accounts/' + loginImage elif os.path.isfile(baseDir + '/accounts/login.webp'): loginImage = 'login.webp' loginImageFilename = baseDir + '/accounts/' + loginImage elif os.path.isfile(baseDir + '/accounts/login.avif'): loginImage = 'login.avif' loginImageFilename = baseDir + '/accounts/' + loginImage if not loginImageFilename: loginImageFilename = baseDir + '/accounts/' + loginImage copyfile(baseDir + '/img/login.png', loginImageFilename) if os.path.isfile(baseDir + '/accounts/login-background-custom.jpg'): if not os.path.isfile(baseDir + '/accounts/login-background.jpg'): copyfile(baseDir + '/accounts/login-background-custom.jpg', baseDir + '/accounts/login-background.jpg') if accounts > 0: loginText = \ '' else: loginText = \ '' loginText += \ '' if os.path.isfile(baseDir + '/accounts/login.txt'): # custom login message with open(baseDir + '/accounts/login.txt', 'r') as file: loginText = '' cssFilename = baseDir + '/epicyon-login.css' if os.path.isfile(baseDir + '/login.css'): cssFilename = baseDir + '/login.css' loginCSS = getCSS(baseDir, cssFilename, cssCache) if not loginCSS: print('ERROR: login css file missing ' + cssFilename) return None # show the register button registerButtonStr = '' if getConfigParam(baseDir, 'registration') == 'open': if int(getConfigParam(baseDir, 'registrationsRemaining')) > 0: if accounts > 0: idx = 'Welcome. Please login or register a new account.' loginText = \ '' registerButtonStr = \ '' TOSstr = \ '' TOSstr += \ '' loginButtonStr = '' if accounts > 0: loginButtonStr = \ '' autocompleteStr = '' if not autocomplete: autocompleteStr = 'autocomplete="off" value=""' loginForm = htmlHeader(cssFilename, loginCSS) loginForm += '
\n' loginForm += '
\n' loginForm += '
\n' loginForm += \ ' login image\n' loginForm += loginText + TOSstr + '\n' loginForm += '
\n' loginForm += '\n' loginForm += '
\n' loginForm += ' \n' loginForm += \ ' \n' loginForm += '\n' loginForm += ' \n' loginForm += \ ' \n' loginForm += loginButtonStr + registerButtonStr + '\n' loginForm += '
\n' loginForm += '
\n' loginForm += \ '' + \ '' + \
        translate['Get the source code'] + '\n' loginForm += htmlFooter() return loginForm def htmlTermsOfService(cssCache: {}, baseDir: str, httpPrefix: str, domainFull: str) -> str: """Show the terms of service screen """ adminNickname = getConfigParam(baseDir, 'admin') if not os.path.isfile(baseDir + '/accounts/tos.txt'): copyfile(baseDir + '/default_tos.txt', baseDir + '/accounts/tos.txt') if os.path.isfile(baseDir + '/accounts/login-background-custom.jpg'): if not os.path.isfile(baseDir + '/accounts/login-background.jpg'): copyfile(baseDir + '/accounts/login-background-custom.jpg', baseDir + '/accounts/login-background.jpg') TOSText = 'Terms of Service go here.' if os.path.isfile(baseDir + '/accounts/tos.txt'): with open(baseDir + '/accounts/tos.txt', 'r') as file: TOSText = file.read() TOSForm = '' cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' termsCSS = getCSS(baseDir, cssFilename, cssCache) if termsCSS: if httpPrefix != 'https': termsCSS = termsCSS.replace('https://', httpPrefix+'://') TOSForm = htmlHeader(cssFilename, termsCSS) TOSForm += '
' + TOSText + '
\n' if adminNickname: adminActor = httpPrefix + '://' + domainFull + \ '/users/' + adminNickname TOSForm += \ '
\n' + \ '

Administered by ' + adminNickname + '

\n' + \ '
\n' TOSForm += htmlFooter() return TOSForm def htmlAbout(cssCache: {}, baseDir: str, httpPrefix: str, domainFull: str, onionDomain: str) -> str: """Show the about screen """ adminNickname = getConfigParam(baseDir, 'admin') if not os.path.isfile(baseDir + '/accounts/about.txt'): copyfile(baseDir + '/default_about.txt', baseDir + '/accounts/about.txt') if os.path.isfile(baseDir + '/accounts/login-background-custom.jpg'): if not os.path.isfile(baseDir + '/accounts/login-background.jpg'): copyfile(baseDir + '/accounts/login-background-custom.jpg', baseDir + '/accounts/login-background.jpg') aboutText = 'Information about this instance goes here.' if os.path.isfile(baseDir + '/accounts/about.txt'): with open(baseDir + '/accounts/about.txt', 'r') as aboutFile: aboutText = aboutFile.read() aboutForm = '' cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' aboutCSS = getCSS(baseDir, cssFilename, cssCache) if aboutCSS: if httpPrefix != 'http': aboutCSS = aboutCSS.replace('https://', httpPrefix + '://') aboutForm = htmlHeader(cssFilename, aboutCSS) aboutForm += '
' + aboutText + '
' if onionDomain: aboutForm += \ '
\n' + \ '

' + \ 'http://' + onionDomain + '

\n
\n' if adminNickname: adminActor = '/users/' + adminNickname aboutForm += \ '
\n' + \ '

Administered by ' + adminNickname + '

\n' + \ '
\n' aboutForm += htmlFooter() return aboutForm def htmlHashtagBlocked(cssCache: {}, baseDir: str, translate: {}) -> str: """Show the screen for a blocked hashtag """ blockedHashtagForm = '' cssFilename = baseDir + '/epicyon-suspended.css' if os.path.isfile(baseDir + '/suspended.css'): cssFilename = baseDir + '/suspended.css' blockedHashtagCSS = getCSS(baseDir, cssFilename, cssCache) if blockedHashtagCSS: blockedHashtagForm = htmlHeader(cssFilename, blockedHashtagCSS) blockedHashtagForm += '
\n' blockedHashtagForm += \ '

' + \ translate['Hashtag Blocked'] + '

\n' blockedHashtagForm += \ '

See ' + \ translate['Terms of Service'] + '

\n' blockedHashtagForm += '
\n' blockedHashtagForm += htmlFooter() return blockedHashtagForm def htmlSuspended(cssCache: {}, baseDir: str) -> str: """Show the screen for suspended accounts """ suspendedForm = '' cssFilename = baseDir + '/epicyon-suspended.css' if os.path.isfile(baseDir + '/suspended.css'): cssFilename = baseDir + '/suspended.css' suspendedCSS = getCSS(baseDir, cssFilename, cssCache) if suspendedCSS: suspendedForm = htmlHeader(cssFilename, suspendedCSS) suspendedForm += '
\n' suspendedForm += '

Account Suspended

\n' suspendedForm += '

See Terms of Service

\n' suspendedForm += '
\n' suspendedForm += htmlFooter() return suspendedForm def htmlNewPostDropDown(scopeIcon: str, scopeDescription: str, replyStr: str, translate: {}, iconsDir: str, showPublicOnDropdown: bool, defaultTimeline: str, pathBase: str, dropdownNewPostSuffix: str, dropdownNewBlogSuffix: str, dropdownUnlistedSuffix: str, dropdownFollowersSuffix: str, dropdownDMSuffix: str, dropdownReminderSuffix: str, dropdownEventSuffix: str, dropdownReportSuffix: str) -> str: """Returns the html for a drop down list of new post types """ dropDownContent = '\n' return dropDownContent def htmlNewPost(cssCache: {}, mediaInstance: bool, translate: {}, baseDir: str, httpPrefix: str, path: str, inReplyTo: str, mentions: [], reportUrl: str, pageNumber: int, nickname: str, domain: str, domainFull: str, defaultTimeline: str, newswire: {}) -> str: """New post screen """ iconsDir = getIconsDir(baseDir) replyStr = '' showPublicOnDropdown = True messageBoxHeight = 400 # filename of the banner shown at the top bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) if not path.endswith('/newshare'): if not path.endswith('/newreport'): if not inReplyTo or path.endswith('/newreminder'): newPostText = '

' + \ translate['Write your post text below.'] + '

\n' else: newPostText = \ '

' + \ translate['Write your reply to'] + \ ' ' + \ translate['this post'] + '

\n' replyStr = '\n' # if replying to a non-public post then also make # this post non-public if not isPublicPostFromUrl(baseDir, nickname, domain, inReplyTo): newPostPath = path if '?' in newPostPath: newPostPath = newPostPath.split('?')[0] if newPostPath.endswith('/newpost'): path = path.replace('/newpost', '/newfollowers') elif newPostPath.endswith('/newunlisted'): path = path.replace('/newunlisted', '/newfollowers') showPublicOnDropdown = False else: newPostText = \ '

' + \ translate['Write your report below.'] + '

\n' # custom report header with any additional instructions if os.path.isfile(baseDir + '/accounts/report.txt'): with open(baseDir + '/accounts/report.txt', 'r') as file: customReportText = file.read() if '

' not in customReportText: customReportText = \ '\n' repStr = '

', repStr) newPostText += customReportText idx = 'This message only goes to moderators, even if it ' + \ 'mentions other fediverse addresses.' newPostText += \ '

' + translate[idx] + '

\n' + \ '

' + translate['Also see'] + \ ' ' + \ translate['Terms of Service'] + '

\n' else: newPostText = \ '

' + \ translate['Enter the details for your shared item below.'] + \ '

\n' if path.endswith('/newquestion'): newPostText = \ '

' + \ translate['Enter the choices for your question below.'] + \ '

\n' if os.path.isfile(baseDir + '/accounts/newpost.txt'): with open(baseDir + '/accounts/newpost.txt', 'r') as file: newPostText = \ '

' + file.read() + '

\n' cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' newPostCSS = getCSS(baseDir, cssFilename, cssCache) if newPostCSS: if httpPrefix != 'https': newPostCSS = newPostCSS.replace('https://', httpPrefix + '://') if '?' in path: path = path.split('?')[0] pathBase = path.replace('/newreport', '').replace('/newpost', '') pathBase = pathBase.replace('/newblog', '').replace('/newshare', '') pathBase = pathBase.replace('/newunlisted', '') pathBase = pathBase.replace('/newevent', '') pathBase = pathBase.replace('/newreminder', '') pathBase = pathBase.replace('/newfollowers', '').replace('/newdm', '') newPostImageSection = '
' if not path.endswith('/newevent'): newPostImageSection += \ ' \n' else: newPostImageSection += \ ' \n' newPostImageSection += \ ' \n' if path.endswith('/newevent'): newPostImageSection += \ ' \n' newPostImageSection += \ ' \n' else: newPostImageSection += \ ' \n' newPostImageSection += '
\n' scopeIcon = 'scope_public.png' scopeDescription = translate['Public'] placeholderSubject = \ translate['Subject or Content Warning (optional)'] + '...' placeholderMentions = '' if inReplyTo: # mentionsAndContent = getMentionsString(content) placeholderMentions = \ translate['Replying to'] + '...' placeholderMessage = translate['Write something'] + '...' extraFields = '' endpoint = 'newpost' if path.endswith('/newblog'): placeholderSubject = translate['Title'] scopeIcon = 'scope_blog.png' if defaultTimeline != 'tlnews': scopeDescription = translate['Blog'] else: scopeDescription = translate['Article'] endpoint = 'newblog' elif path.endswith('/newunlisted'): scopeIcon = 'scope_unlisted.png' scopeDescription = translate['Unlisted'] endpoint = 'newunlisted' elif path.endswith('/newfollowers'): scopeIcon = 'scope_followers.png' scopeDescription = translate['Followers'] endpoint = 'newfollowers' elif path.endswith('/newdm'): scopeIcon = 'scope_dm.png' scopeDescription = translate['DM'] endpoint = 'newdm' elif path.endswith('/newreminder'): scopeIcon = 'scope_reminder.png' scopeDescription = translate['Reminder'] endpoint = 'newreminder' elif path.endswith('/newevent'): scopeIcon = 'scope_event.png' scopeDescription = translate['Event'] endpoint = 'newevent' placeholderSubject = translate['Event name'] placeholderMessage = translate['Describe the event'] + '...' elif path.endswith('/newreport'): scopeIcon = 'scope_report.png' scopeDescription = translate['Report'] endpoint = 'newreport' elif path.endswith('/newquestion'): scopeIcon = 'scope_question.png' scopeDescription = translate['Question'] placeholderMessage = translate['Enter your question'] + '...' endpoint = 'newquestion' extraFields = '
\n' extraFields += '
\n' for questionCtr in range(8): extraFields += \ '
\n' extraFields += \ '
\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 += \ ' \n' extraFields += \ ' \n' extraFields += \ '
\n' extraFields += \ ' \n' extraFields += \ '
\n' extraFields += ' \n' extraFields += '
\n' extraFields += '
\n' extraFields += \ '\n' extraFields += '\n' extraFields += '
\n' citationsStr = '' if endpoint == 'newblog': citationsFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/.citations.txt' if os.path.isfile(citationsFilename): citationsStr = '
\n' citationsStr += '

\n' citationsStr += '
    \n' citationsSeparator = '#####' with open(citationsFilename, "r") as f: citations = f.readlines() for line in citations: if citationsSeparator not in line: continue sections = line.strip().split(citationsSeparator) if len(sections) != 3: continue title = sections[1] link = sections[2] citationsStr += \ '
  • ' + \ title + '
  • ' citationsStr += '
\n' citationsStr += '
\n' dateAndLocation = '' if endpoint != 'newshare' and \ endpoint != 'newreport' and \ endpoint != 'newquestion': dateAndLocation = '
\n' if endpoint == 'newevent': # event status dateAndLocation += '
\n' dateAndLocation += '\n' dateAndLocation += '
\n' dateAndLocation += '\n' dateAndLocation += '
\n' dateAndLocation += '\n' dateAndLocation += '
\n' dateAndLocation += '
\n' dateAndLocation += '
\n' # maximum attendees dateAndLocation += '\n' dateAndLocation += '\n' dateAndLocation += '
\n' dateAndLocation += '
\n' # event joining options dateAndLocation += '
\n' dateAndLocation += '\n' dateAndLocation += '
\n' dateAndLocation += '\n' dateAndLocation += '
\n' dateAndLocation += '\n' dateAndLocation += '\n' dateAndLocation += '
\n' dateAndLocation += '
\n' # Event posts don't allow replies - they're just an announcement. # They also have a few more checkboxes dateAndLocation += \ '

\n' dateAndLocation += \ '

' + \ '

\n' else: dateAndLocation += \ '

\n' if not inReplyTo and endpoint != 'newevent': dateAndLocation += \ '

\n' if endpoint != 'newevent': dateAndLocation += \ '

\n' # select a date and time for this post dateAndLocation += '\n' dateAndLocation += '\n' dateAndLocation += '

\n' else: dateAndLocation += '
\n' dateAndLocation += '
\n' dateAndLocation += \ '

\n' # select start time for the event dateAndLocation += '\n' dateAndLocation += '\n' dateAndLocation += '

\n' # select end time for the event dateAndLocation += \ '
\n' dateAndLocation += '\n' dateAndLocation += '\n' dateAndLocation += '\n' if endpoint == 'newevent': dateAndLocation += '
\n' dateAndLocation += '
\n' dateAndLocation += '
\n' dateAndLocation += \ ' \n' dateAndLocation += '
\n' dateAndLocation += '
\n' dateAndLocation += '
\n' dateAndLocation += '\n' if endpoint == 'newevent': dateAndLocation += '
\n' dateAndLocation += '\n' dateAndLocation += '
\n' dateAndLocation += '\n' dateAndLocation += '
\n' newPostForm = htmlHeader(cssFilename, newPostCSS) newPostForm += \ '\n' newPostForm += '\n' mentionsStr = '' for m in mentions: mentionNickname = getNicknameFromActor(m) if not mentionNickname: continue mentionDomain, mentionPort = getDomainFromActor(m) if not mentionDomain: continue if mentionPort: mentionsHandle = \ '@' + mentionNickname + '@' + \ mentionDomain + ':' + str(mentionPort) else: mentionsHandle = '@' + mentionNickname + '@' + mentionDomain if mentionsHandle not in mentionsStr: mentionsStr += mentionsHandle + ' ' # build suffixes so that any replies or mentions are # preserved when switching between scopes dropdownNewPostSuffix = '/newpost' dropdownNewBlogSuffix = '/newblog' dropdownUnlistedSuffix = '/newunlisted' dropdownFollowersSuffix = '/newfollowers' dropdownDMSuffix = '/newdm' dropdownEventSuffix = '/newevent' dropdownReminderSuffix = '/newreminder' dropdownReportSuffix = '/newreport' if inReplyTo or mentions: dropdownNewPostSuffix = '' dropdownNewBlogSuffix = '' dropdownUnlistedSuffix = '' dropdownFollowersSuffix = '' dropdownDMSuffix = '' dropdownEventSuffix = '' dropdownReminderSuffix = '' dropdownReportSuffix = '' if inReplyTo: dropdownNewPostSuffix += '?replyto=' + inReplyTo dropdownNewBlogSuffix += '?replyto=' + inReplyTo dropdownUnlistedSuffix += '?replyto=' + inReplyTo dropdownFollowersSuffix += '?replyfollowers=' + inReplyTo dropdownDMSuffix += '?replydm=' + inReplyTo for mentionedActor in mentions: dropdownNewPostSuffix += '?mention=' + mentionedActor dropdownNewBlogSuffix += '?mention=' + mentionedActor dropdownUnlistedSuffix += '?mention=' + mentionedActor dropdownFollowersSuffix += '?mention=' + mentionedActor dropdownDMSuffix += '?mention=' + mentionedActor dropdownReportSuffix += '?mention=' + mentionedActor dropDownContent = '' if not reportUrl: dropDownContent = \ htmlNewPostDropDown(scopeIcon, scopeDescription, replyStr, translate, iconsDir, showPublicOnDropdown, defaultTimeline, pathBase, dropdownNewPostSuffix, dropdownNewBlogSuffix, dropdownUnlistedSuffix, dropdownFollowersSuffix, dropdownDMSuffix, dropdownReminderSuffix, dropdownEventSuffix, dropdownReportSuffix) else: mentionsStr = 'Re: ' + reportUrl + '\n\n' + mentionsStr newPostForm += \ '
\n' newPostForm += '
\n' newPostForm += \ ' \n' newPostForm += '
\n' newPostForm += ' \n' newPostForm += '\n' newPostForm += \ ' \n' newPostForm += ' \n' newPostForm += '
' + dropDownContent + '' + \
        translate['Search for emoji'] + '
\n' newPostForm += '
\n' newPostForm += '
\n' # newPostForm += \ # ' \n' # for a new blog if newswire items exist then add a citations button if newswire and path.endswith('/newblog'): newPostForm += \ ' \n' newPostForm += \ ' \n' newPostForm += '
\n' newPostForm += replyStr if mediaInstance and not replyStr: newPostForm += newPostImageSection newPostForm += \ '
' newPostForm += ' ' newPostForm += '' selectedStr = ' selected' if inReplyTo or endpoint == 'newdm': if inReplyTo: newPostForm += \ '
\n' else: newPostForm += \ ' ' \ ' 📄
\n' newPostForm += \ ' \n' newPostForm += \ htmlFollowingDataList(baseDir, nickname, domain, domainFull) newPostForm += '' selectedStr = '' newPostForm += \ '
' if mediaInstance: messageBoxHeight = 200 if endpoint == 'newquestion': messageBoxHeight = 100 elif endpoint == 'newblog': messageBoxHeight = 800 newPostForm += \ ' \n' newPostForm += extraFields + citationsStr + dateAndLocation if not mediaInstance or replyStr: newPostForm += newPostImageSection newPostForm += '
\n' newPostForm += '
\n' if not reportUrl: newPostForm = \ newPostForm.replace('', '') newPostForm += htmlFooter() return newPostForm def htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int, translate: {}, baseDir: str, httpPrefix: str, authorized: bool, nickname: str, domain: str, port: int, session, wfRequest: {}, personCache: {}, projectVersion: str, YTReplacementDomain: str, showPublishedDateOnly: bool) -> str: """Shows posts on the profile screen These should only be public posts """ iconsDir = getIconsDir(baseDir) separatorStr = htmlPostSeparator(baseDir, None) profileStr = '' maxItems = 4 ctr = 0 currPage = 1 while ctr < maxItems and currPage < 4: outboxFeed = \ personBoxJson({}, session, baseDir, domain, port, '/users/' + nickname + '/outbox?page=' + str(currPage), httpPrefix, 10, 'outbox', authorized, 0, False, 0) if not outboxFeed: break if len(outboxFeed['orderedItems']) == 0: break for item in outboxFeed['orderedItems']: if item['type'] == 'Create': postStr = \ individualPostAsHtml(True, recentPostsCache, maxRecentPosts, iconsDir, translate, None, baseDir, session, wfRequest, personCache, nickname, domain, port, item, None, True, False, httpPrefix, projectVersion, 'inbox', YTReplacementDomain, showPublishedDateOnly, False, False, False, True, False) if postStr: profileStr += separatorStr + postStr ctr += 1 if ctr >= maxItems: break currPage += 1 return profileStr def htmlProfileFollowing(translate: {}, baseDir: str, httpPrefix: str, authorized: bool, nickname: str, domain: str, port: int, session, wfRequest: {}, personCache: {}, followingJson: {}, projectVersion: str, buttons: [], feedName: str, actor: str, pageNumber: int, maxItemsPerPage: int) -> str: """Shows following on the profile screen """ profileStr = '' iconsDir = getIconsDir(baseDir) if authorized and pageNumber: if authorized and pageNumber > 1: # page up arrow profileStr += \ '
\n' + \ ' ' + \
                translate['Page up'] + '\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' + \ ' ' + \
                translate['Page down'] + '\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 += \ '
' + skill + \ '
\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 += '\n' if item.get('imageUrl'): profileStr += '\n' profileStr += \ '' + translate['Item image'] + '\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 += \ '

\n' if removeButton: profileStr += \ ' \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 = '\n' return profileStr def htmlSharesTimeline(translate: {}, pageNumber: int, itemsPerPage: int, baseDir: str, actor: str, nickname: str, domain: str, port: int, maxSharesPerAccount: int, httpPrefix: str) -> str: """Show shared items timeline as html """ sharesJson, lastPage = \ sharesTimelineJson(actor, pageNumber, itemsPerPage, baseDir, maxSharesPerAccount) domainFull = domain if port != 80 and port != 443: if ':' not in domain: domainFull = domain + ':' + str(port) actor = httpPrefix + '://' + domainFull + '/users/' + nickname timelineStr = '' if pageNumber > 1: iconsDir = getIconsDir(baseDir) timelineStr += \ '
\n' + \ ' ' + translate['Page up'] + '\n' + \ '
\n' separatorStr = htmlPostSeparator(baseDir, None) for published, item in sharesJson.items(): showContactButton = False if item['actor'] != actor: showContactButton = True showRemoveButton = False if item['actor'] == actor: showRemoveButton = True timelineStr += separatorStr + \ htmlIndividualShare(actor, item, translate, showContactButton, showRemoveButton) if not lastPage: iconsDir = getIconsDir(baseDir) timelineStr += \ '
\n' + \ ' ' + translate['Page down'] + '\n' + \ '
\n' return timelineStr def htmlProfile(rssIconAtTop: bool, cssCache: {}, iconsAsButtons: bool, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, projectVersion: str, baseDir: str, httpPrefix: str, authorized: bool, profileJson: {}, selected: str, session, wfRequest: {}, personCache: {}, YTReplacementDomain: str, showPublishedDateOnly: bool, newswire: {}, extraJson=None, pageNumber=None, maxItemsPerPage=None) -> str: """Show the profile page as html """ nickname = profileJson['preferredUsername'] if not nickname: return "" domain, port = getDomainFromActor(profileJson['id']) if not domain: return "" displayName = \ addEmojiToDisplayName(baseDir, httpPrefix, nickname, domain, profileJson['name'], True) domainFull = domain if port: domainFull = domain + ':' + str(port) profileDescription = \ addEmojiToDisplayName(baseDir, httpPrefix, nickname, domain, profileJson['summary'], False) postsButton = 'button' followingButton = 'button' followersButton = 'button' rolesButton = 'button' skillsButton = 'button' sharesButton = 'button' if selected == 'posts': postsButton = 'buttonselected' elif selected == 'following': followingButton = 'buttonselected' elif selected == 'followers': followersButton = 'buttonselected' elif selected == 'roles': rolesButton = 'buttonselected' elif selected == 'skills': skillsButton = 'buttonselected' elif selected == 'shares': sharesButton = 'buttonselected' loginButton = '' followApprovalsSection = '' followApprovals = False linkToTimelineStart = '' linkToTimelineEnd = '' editProfileStr = '' logoutStr = '' actor = profileJson['id'] usersPath = '/users/' + actor.split('/users/')[1] donateSection = '' donateUrl = getDonationUrl(profileJson) PGPpubKey = getPGPpubKey(profileJson) PGPfingerprint = getPGPfingerprint(profileJson) emailAddress = getEmailAddress(profileJson) xmppAddress = getXmppAddress(profileJson) matrixAddress = getMatrixAddress(profileJson) ssbAddress = getSSBAddress(profileJson) toxAddress = getToxAddress(profileJson) if donateUrl or xmppAddress or matrixAddress or \ ssbAddress or toxAddress or PGPpubKey or \ PGPfingerprint or emailAddress: donateSection = '
\n' donateSection += '
\n' if donateUrl and not isSystemAccount(nickname): donateSection += \ '

\n' if emailAddress: donateSection += \ '

' + translate['Email'] + ': ' + emailAddress + '

\n' if xmppAddress: donateSection += \ '

' + translate['XMPP'] + ': '+xmppAddress + '

\n' if matrixAddress: donateSection += \ '

' + translate['Matrix'] + ': ' + matrixAddress + '

\n' if ssbAddress: donateSection += \ '

SSB:

\n' if toxAddress: donateSection += \ '

Tox:

\n' if PGPfingerprint: donateSection += \ '

PGP: ' + \ PGPfingerprint.replace('\n', '
') + '

\n' if PGPpubKey: donateSection += \ '

' + PGPpubKey.replace('\n', '
') + '

\n' donateSection += '
\n' donateSection += '
\n' iconsDir = getIconsDir(baseDir) if not authorized: loginButton = headerButtonsFrontScreen(translate, nickname, 'features', authorized, iconsAsButtons, iconsDir) else: editProfileStr = \ '' + \ '| ' + translate['Edit'] + '\n' logoutStr = \ '' + \ '| ' + translate['Logout'] + \
            '\n' linkToTimelineStart = \ '' linkToTimelineStart += \ '' linkToTimelineEnd = '' # are there any follow requests? followRequestsFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/followrequests.txt' if os.path.isfile(followRequestsFilename): with open(followRequestsFilename, 'r') as f: for line in f: if len(line) > 0: followApprovals = True followersButton = 'buttonhighlighted' if selected == 'followers': followersButton = 'buttonselectedhighlighted' break if selected == 'followers': if followApprovals: with open(followRequestsFilename, 'r') as f: for followerHandle in f: if len(line) > 0: if '://' in followerHandle: followerActor = followerHandle else: followerActor = \ httpPrefix + '://' + \ followerHandle.split('@')[1] + \ '/users/' + followerHandle.split('@')[0] basePath = '/users/' + nickname followApprovalsSection += '
' followApprovalsSection += \ '' followApprovalsSection += \ '' + \ followerHandle + '' followApprovalsSection += \ '' followApprovalsSection += \ '

' followApprovalsSection += \ '' followApprovalsSection += \ '' followApprovalsSection += '
' profileDescriptionShort = profileDescription if '\n' in profileDescription: if len(profileDescription.split('\n')) > 2: profileDescriptionShort = '' else: if '
' in profileDescription: if len(profileDescription.split('
')) > 2: profileDescriptionShort = '' profileDescription = profileDescription.replace('
', '\n') # keep the profile description short if len(profileDescriptionShort) > 256: profileDescriptionShort = '' # remove formatting from profile description used on title avatarDescription = '' if profileJson.get('summary'): avatarDescription = profileJson['summary'].replace('
', '\n') avatarDescription = avatarDescription.replace('

', '') avatarDescription = avatarDescription.replace('

', '') # If this is the news account then show a different banner if isSystemAccount(nickname): bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) profileHeaderStr = \ '\n' if loginButton: profileHeaderStr += '
' + loginButton + '
\n' profileHeaderStr += '\n' profileHeaderStr += ' \n' profileHeaderStr += ' \n' profileHeaderStr += ' \n' profileHeaderStr += ' \n' profileHeaderStr += ' \n' profileHeaderStr += ' \n' profileHeaderStr += ' \n' profileHeaderStr += ' \n' profileHeaderStr += ' \n' profileFooterStr += ' \n' profileFooterStr += ' \n' profileFooterStr += ' \n' profileFooterStr += '
\n' iconsDir = getIconsDir(baseDir) profileHeaderStr += \ getLeftColumnContent(baseDir, 'news', domainFull, httpPrefix, translate, iconsDir, False, False, None, rssIconAtTop, True, True) profileHeaderStr += ' \n' else: profileHeaderStr = '
\n' profileHeaderStr += '
\n' profileHeaderStr += \ ' ' + \
            avatarDescription + '\n' profileHeaderStr += '

' + displayName + '

\n' iconsDir = getIconsDir(baseDir) profileHeaderStr += \ '

@' + nickname + '@' + domainFull + '
' profileHeaderStr += \ '' + \ '

\n' profileHeaderStr += '

' + profileDescriptionShort + '

\n' profileHeaderStr += loginButton profileHeaderStr += '
\n' profileHeaderStr += '
\n' profileStr = \ linkToTimelineStart + profileHeaderStr + \ linkToTimelineEnd + donateSection if not isSystemAccount(nickname): profileStr += '
\n' profileStr += '
' profileStr += \ ' ' profileStr += \ ' ' + \ '' profileStr += \ ' ' + \ '' profileStr += \ ' ' + \ '' profileStr += \ ' ' + \ '' profileStr += \ ' ' + \ '' profileStr += logoutStr + editProfileStr profileStr += '
' profileStr += '
' profileStr += followApprovalsSection cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' profileStyle = getCSS(baseDir, cssFilename, cssCache) if profileStyle: profileStyle = \ profileStyle.replace('image.png', profileJson['image']['url']) if isSystemAccount(nickname): bannerFile, bannerFilename = \ getBannerFile(baseDir, nickname, domain) profileStyle = \ profileStyle.replace('banner.png', '/users/' + nickname + '/' + bannerFile) licenseStr = \ '' + \ '' + \
            translate['Get the source code'] + '' if selected == 'posts': profileStr += \ htmlProfilePosts(recentPostsCache, maxRecentPosts, translate, baseDir, httpPrefix, authorized, nickname, domain, port, session, wfRequest, personCache, projectVersion, YTReplacementDomain, showPublishedDateOnly) + licenseStr if selected == 'following': profileStr += \ htmlProfileFollowing(translate, baseDir, httpPrefix, authorized, nickname, domain, port, session, wfRequest, personCache, extraJson, projectVersion, ["unfollow"], selected, usersPath, pageNumber, maxItemsPerPage) if selected == 'followers': profileStr += \ htmlProfileFollowing(translate, baseDir, httpPrefix, authorized, nickname, domain, port, session, wfRequest, personCache, extraJson, projectVersion, ["block"], selected, usersPath, pageNumber, maxItemsPerPage) if selected == 'roles': profileStr += \ htmlProfileRoles(translate, nickname, domainFull, extraJson) if selected == 'skills': profileStr += \ htmlProfileSkills(translate, nickname, domainFull, extraJson) if selected == 'shares': profileStr += \ htmlProfileShares(actor, translate, nickname, domainFull, extraJson) + licenseStr # Footer which is only used for system accounts profileFooterStr = '' if isSystemAccount(nickname): profileFooterStr = '
\n' iconsDir = getIconsDir(baseDir) profileFooterStr += \ getRightColumnContent(baseDir, 'news', domainFull, httpPrefix, translate, iconsDir, False, False, newswire, False, False, None, False, False, False, True, authorized, True) profileFooterStr += '
\n' profileStr = \ htmlHeader(cssFilename, profileStyle) + \ profileStr + profileFooterStr + htmlFooter() return profileStr def individualFollowAsHtml(translate: {}, baseDir: str, session, wfRequest: {}, personCache: {}, domain: str, followUrl: str, authorized: bool, actorNickname: str, httpPrefix: str, projectVersion: str, buttons=[]) -> str: """An individual follow entry on the profile screen """ nickname = getNicknameFromActor(followUrl) domain, port = getDomainFromActor(followUrl) titleStr = '@' + nickname + '@' + domain avatarUrl = getPersonAvatarUrl(baseDir, followUrl, personCache, True) if not avatarUrl: avatarUrl = followUrl + '/avatar.png' if domain not in followUrl: (inboxUrl, pubKeyId, pubKey, fromPersonId, sharedInbox, avatarUrl2, displayName) = getPersonBox(baseDir, session, wfRequest, personCache, projectVersion, httpPrefix, nickname, domain, 'outbox') if avatarUrl2: avatarUrl = avatarUrl2 if displayName: titleStr = displayName + ' ' + titleStr buttonsStr = '' if authorized: for b in buttons: if b == 'block': buttonsStr += \ '\n' if b == 'unfollow': buttonsStr += \ '\n' resultStr = '
\n' resultStr += \ '\n' resultStr += '

 ' resultStr += titleStr + '' + buttonsStr + '

\n' resultStr += '
\n' return resultStr def htmlHighlightLabel(label: str, highlight: bool) -> str: """If the give text should be highlighted then return the appropriate markup. This is so that in shell browsers, like lynx, it's possible to see if the replies or DM button are highlighted. """ if not highlight: return label return '*' + str(label) + '*' def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str, httpPrefix: str, translate: {}, iconsDir: str, editor: bool, showBackButton: bool, timelinePath: str, rssIconAtTop: bool, showHeaderImage: bool, frontPage: bool) -> str: """Returns html content for the left column """ htmlStr = '' separatorStr = htmlPostSeparator(baseDir, 'left') domain = domainFull if ':' in domain: domain = domain.split(':') editImageClass = '' if showHeaderImage: leftImageFile, leftColumnImageFilename = \ getLeftImageFile(baseDir, nickname, domain) if not os.path.isfile(leftColumnImageFilename): theme = getConfigParam(baseDir, 'theme').lower() if theme == 'default': theme = '' else: theme = '_' + theme themeLeftImageFile, themeLeftColumnImageFilename = \ getImageFile(baseDir, 'left_col_image', baseDir + '/img', nickname, domain) if os.path.isfile(themeLeftColumnImageFilename): leftColumnImageFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/' + themeLeftImageFile copyfile(themeLeftColumnImageFilename, leftColumnImageFilename) leftImageFile = themeLeftImageFile # show the image at the top of the column editImageClass = 'leftColEdit' if os.path.isfile(leftColumnImageFilename): editImageClass = 'leftColEditImage' htmlStr += \ '\n
\n' + \ ' \n' + \ '
\n' if showBackButton: htmlStr += \ '
' + \ ' ' + \ '\n' if (editor or rssIconAtTop) and not showHeaderImage: htmlStr += '
' if editImageClass == 'leftColEdit': htmlStr += '\n
\n' htmlStr += '
\n' if editor: # show the edit icon htmlStr += \ ' ' + \ '' + \
            translate['Edit Links'] + '\n' # RSS icon if nickname != 'news': # rss feed for this account rssUrl = httpPrefix + '://' + domainFull + \ '/blog/' + nickname + '/rss.xml' else: # rss feed for all accounts on the instance rssUrl = httpPrefix + '://' + domainFull + '/blog/rss.xml' if not frontPage: rssTitle = translate['RSS feed for your blog'] else: rssTitle = translate['RSS feed for this site'] rssIconStr = \ ' ' + \ '' + rssTitle + \
        '\n' if rssIconAtTop: htmlStr += rssIconStr htmlStr += '
\n' if editImageClass == 'leftColEdit': htmlStr += '
\n' if (editor or rssIconAtTop) and not showHeaderImage: htmlStr += '

' # if showHeaderImage: # htmlStr += '
' linksFilename = baseDir + '/accounts/links.txt' linksFileContainsEntries = False if os.path.isfile(linksFilename): linksList = None with open(linksFilename, "r") as f: linksList = f.readlines() if linksList: for lineStr in linksList: if ' ' not in lineStr: if '#' not in lineStr: if '*' not in lineStr: continue lineStr = lineStr.strip() words = lineStr.split(' ') # get the link linkStr = None for word in words: if word == '#': continue if word == '*': continue if '://' in word: linkStr = word break if linkStr: lineStr = lineStr.replace(linkStr, '').strip() # avoid any dubious scripts being added if '<' not in lineStr: # remove trailing comma if present if lineStr.endswith(','): lineStr = lineStr[:len(lineStr)-1] # add link to the returned html htmlStr += \ '

' + \ lineStr + '

\n' linksFileContainsEntries = True else: if lineStr.startswith('#') or lineStr.startswith('*'): lineStr = lineStr[1:].strip() htmlStr += separatorStr htmlStr += \ '

' + \ lineStr + '

\n' else: htmlStr += \ '

' + lineStr + '

\n' linksFileContainsEntries = True if linksFileContainsEntries and not rssIconAtTop: htmlStr += '
' + rssIconStr + '
' return htmlStr def votesIndicator(totalVotes: int, positiveVoting: bool) -> str: """Returns an indicator of the number of votes on a newswire item """ if totalVotes <= 0: return '' totalVotesStr = ' ' for v in range(totalVotes): if positiveVoting: totalVotesStr += '✓' else: totalVotesStr += '✗' return totalVotesStr def htmlNewswire(baseDir: str, newswire: {}, nickname: str, moderator: bool, translate: {}, positiveVoting: bool, iconsDir: str) -> str: """Converts a newswire dict into html """ separatorStr = htmlPostSeparator(baseDir, 'right') htmlStr = '' for dateStr, item in newswire.items(): publishedDate = \ datetime.strptime(dateStr, "%Y-%m-%d %H:%M:%S%z") dateShown = publishedDate.strftime("%Y-%m-%d %H:%M") dateStrLink = dateStr.replace('T', ' ') dateStrLink = dateStrLink.replace('Z', '') moderatedItem = item[5] htmlStr += separatorStr if moderatedItem and 'vote:' + nickname in item[2]: totalVotesStr = '' totalVotes = 0 if moderator: totalVotes = votesOnNewswireItem(item[2]) totalVotesStr = \ votesIndicator(totalVotes, positiveVoting) title = removeLongWords(item[0], 16, []).replace('\n', '
') htmlStr += '

' + \ '' + \ '' + title + \ '' + totalVotesStr if moderator: htmlStr += \ ' ' + dateShown + '' htmlStr += '

\n' else: htmlStr += ' ' htmlStr += dateShown + '

\n' else: totalVotesStr = '' totalVotes = 0 if moderator: if moderatedItem: totalVotes = votesOnNewswireItem(item[2]) # show a number of ticks or crosses for how many # votes for or against totalVotesStr = \ votesIndicator(totalVotes, positiveVoting) title = removeLongWords(item[0], 16, []).replace('\n', '
') if moderator and moderatedItem: htmlStr += '

' + \ '' + \ title + '' + totalVotesStr htmlStr += ' ' + dateShown htmlStr += '' htmlStr += '' htmlStr += '

\n' else: htmlStr += '

' + \ '' + \ title + '' + \ totalVotesStr htmlStr += ' ' htmlStr += dateShown + '

\n' return htmlStr def htmlCitations(baseDir: str, nickname: str, domain: str, httpPrefix: str, defaultTimeline: str, translate: {}, newswire: {}, cssCache: {}, blogTitle: str, blogContent: str, blogImageFilename: str, blogImageAttachmentMediaType: str, blogImageDescription: str) -> str: """Show the citations screen when creating a blog """ htmlStr = '' # create a list of dates for citations # these can then be used to re-select checkboxes later citationsFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/.citations.txt' citationsSelected = [] if os.path.isfile(citationsFilename): citationsSeparator = '#####' with open(citationsFilename, "r") as f: citations = f.readlines() for line in citations: if citationsSeparator not in line: continue sections = line.strip().split(citationsSeparator) if len(sections) != 3: continue dateStr = sections[0] citationsSelected.append(dateStr) # the css filename cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' profileStyle = getCSS(baseDir, cssFilename, cssCache) if profileStyle: # replace any https within the css with whatever prefix is needed if httpPrefix != 'https': profileStyle = \ profileStyle.replace('https://', httpPrefix + '://') # iconsDir = getIconsDir(baseDir) htmlStr = htmlHeader(cssFilename, profileStyle) # top banner bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) htmlStr += \ '\n' htmlStr += '\n' htmlStr += \ '
\n' htmlStr += '
\n' htmlStr += translate['Choose newswire items ' + 'referenced in your article'] + '
' if blogTitle is None: blogTitle = '' htmlStr += \ ' \n' if blogContent is None: blogContent = '' htmlStr += \ ' \n' # submit button htmlStr += \ ' \n' htmlStr += '
\n' citationsSeparator = '#####' # list of newswire items if newswire: ctr = 0 for dateStr, item in newswire.items(): # should this checkbox be selected? selectedStr = '' if dateStr in citationsSelected: selectedStr = ' checked' publishedDate = \ datetime.strptime(dateStr, "%Y-%m-%d %H:%M:%S%z") dateShown = publishedDate.strftime("%Y-%m-%d %H:%M") title = removeLongWords(item[0], 16, []).replace('\n', '
') link = item[1] citationValue = \ dateStr + citationsSeparator + \ title + citationsSeparator + \ link htmlStr += \ '' + \ '' + title + ' ' htmlStr += '' + \ dateShown + '
\n' ctr += 1 htmlStr += '
\n' return htmlStr + htmlFooter() def getRightColumnContent(baseDir: str, nickname: str, domainFull: str, httpPrefix: str, translate: {}, iconsDir: str, moderator: bool, editor: bool, newswire: {}, positiveVoting: bool, showBackButton: bool, timelinePath: str, showPublishButton: bool, showPublishAsIcon: bool, rssIconAtTop: bool, publishButtonAtTop: bool, authorized: bool, showHeaderImage: bool) -> str: """Returns html content for the right column """ htmlStr = '' domain = domainFull if ':' in domain: domain = domain.split(':') if authorized: # only show the publish button if logged in, otherwise replace it with # a login button publishButtonStr = \ ' ' + \ '\n' else: # if not logged in then replace the publish button with # a login button publishButtonStr = \ ' \n' # show publish button at the top if needed if publishButtonAtTop: htmlStr += '
' + publishButtonStr + '
' # show a column header image, eg. title of the theme or newswire banner editImageClass = '' if showHeaderImage: rightImageFile, rightColumnImageFilename = \ getRightImageFile(baseDir, nickname, domain) if not os.path.isfile(rightColumnImageFilename): theme = getConfigParam(baseDir, 'theme').lower() if theme == 'default': theme = '' else: theme = '_' + theme themeRightImageFile, themeRightColumnImageFilename = \ getImageFile(baseDir, 'right_col_image', baseDir + '/img', nickname, domain) if os.path.isfile(themeRightColumnImageFilename): rightColumnImageFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/' + themeRightImageFile copyfile(themeRightColumnImageFilename, rightColumnImageFilename) rightImageFile = themeRightImageFile # show the image at the top of the column editImageClass = 'rightColEdit' if os.path.isfile(rightColumnImageFilename): editImageClass = 'rightColEditImage' htmlStr += \ '\n
\n' + \ ' \n' + \ '
\n' if (showPublishButton or editor or rssIconAtTop) and not showHeaderImage: htmlStr += '
' if editImageClass == 'rightColEdit': htmlStr += '\n
\n' # whether to show a back icon # This is probably going to be osolete soon if showBackButton: htmlStr += \ ' ' + \ '\n' if showPublishButton and not publishButtonAtTop: if not showPublishAsIcon: htmlStr += publishButtonStr # show the edit icon if editor: if os.path.isfile(baseDir + '/accounts/newswiremoderation.txt'): # show the edit icon highlighted htmlStr += \ ' ' + \ '' + \
                translate['Edit newswire'] + '\n' else: # show the edit icon htmlStr += \ ' ' + \ '' + \
                translate['Edit newswire'] + '\n' # show the RSS icon rssIconStr = \ ' ' + \ '' + \
        translate['Newswire RSS Feed'] + '\n' if rssIconAtTop: htmlStr += rssIconStr # show publish icon at top if showPublishButton: if showPublishAsIcon: htmlStr += \ ' ' + \ '' + \
                translate['Publish a news article'] + '\n' if editImageClass == 'rightColEdit': htmlStr += '
\n' else: if showHeaderImage: htmlStr += '
\n' if (showPublishButton or editor or rssIconAtTop) and not showHeaderImage: htmlStr += '

' # show the newswire lines newswireContentStr = \ htmlNewswire(baseDir, newswire, nickname, moderator, translate, positiveVoting, iconsDir) htmlStr += newswireContentStr # show the rss icon at the bottom, typically on the right hand side if newswireContentStr and not rssIconAtTop: htmlStr += '
' + rssIconStr + '
' return htmlStr def htmlLinksMobile(cssCache: {}, baseDir: str, nickname: str, domainFull: str, httpPrefix: str, translate, timelinePath: str, authorized: bool, rssIconAtTop: bool, iconsAsButtons: bool, defaultTimeline: str) -> str: """Show the left column links within mobile view """ htmlStr = '' # the css filename cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' profileStyle = getCSS(baseDir, cssFilename, cssCache) if profileStyle: # replace any https within the css with whatever prefix is needed if httpPrefix != 'https': profileStyle = \ profileStyle.replace('https://', httpPrefix + '://') iconsDir = getIconsDir(baseDir) # is the user a site editor? if nickname == 'news': editor = False else: editor = isEditor(baseDir, nickname) domain = domainFull if ':' in domain: domain = domain.split(':')[0] htmlStr = htmlHeader(cssFilename, profileStyle) bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) htmlStr += \ '' + \ '\n' htmlStr += '
' + \ headerButtonsFrontScreen(translate, nickname, 'links', authorized, iconsAsButtons, iconsDir) + '
' htmlStr += \ getLeftColumnContent(baseDir, nickname, domainFull, httpPrefix, translate, iconsDir, editor, False, timelinePath, rssIconAtTop, False, False) htmlStr += '
\n' + htmlFooter() return htmlStr def htmlNewswireMobile(cssCache: {}, baseDir: str, nickname: str, domain: str, domainFull: str, httpPrefix: str, translate: {}, newswire: {}, positiveVoting: bool, timelinePath: str, showPublishAsIcon: bool, authorized: bool, rssIconAtTop: bool, iconsAsButtons: bool, defaultTimeline: str) -> str: """Shows the mobile version of the newswire right column """ htmlStr = '' # the css filename cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' profileStyle = getCSS(baseDir, cssFilename, cssCache) if profileStyle: # replace any https within the css with whatever prefix is needed if httpPrefix != 'https': profileStyle = \ profileStyle.replace('https://', httpPrefix + '://') iconsDir = getIconsDir(baseDir) if nickname == 'news': editor = False moderator = False else: # is the user a moderator? moderator = isModerator(baseDir, nickname) # is the user a site editor? editor = isEditor(baseDir, nickname) showPublishButton = editor htmlStr = htmlHeader(cssFilename, profileStyle) bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) htmlStr += \ '' + \ '\n' htmlStr += '
' + \ headerButtonsFrontScreen(translate, nickname, 'newswire', authorized, iconsAsButtons, iconsDir) + '
' htmlStr += \ getRightColumnContent(baseDir, nickname, domainFull, httpPrefix, translate, iconsDir, moderator, editor, newswire, positiveVoting, False, timelinePath, showPublishButton, showPublishAsIcon, rssIconAtTop, False, authorized, False) htmlStr += htmlFooter() return htmlStr def headerButtonsFrontScreen(translate: {}, nickname: str, boxName: str, authorized: bool, iconsAsButtons: bool, iconsDir: bool) -> str: """Returns the header buttons for the front page of a news instance """ headerStr = '' if nickname == 'news': buttonFeatures = 'buttonMobile' buttonNewswire = 'buttonMobile' buttonLinks = 'buttonMobile' if boxName == 'features': buttonFeatures = 'buttonselected' elif boxName == 'newswire': buttonNewswire = 'buttonselected' elif boxName == 'links': buttonLinks = 'buttonselected' headerStr += \ ' ' + \ '' if not authorized: headerStr += \ ' ' + \ '' if iconsAsButtons: headerStr += \ ' ' + \ '' headerStr += \ ' ' + \ '' else: headerStr += \ ' ' + \ '| ' + translate['Newswire'] + '\n' headerStr += \ ' ' + \ '| ' + translate['Links'] + '\n' else: if not authorized: headerStr += \ ' ' + \ '' if headerStr: headerStr = \ '\n
\n' + \ headerStr + \ '
\n' return headerStr def headerButtonsTimeline(defaultTimeline: str, boxName: str, pageNumber: int, translate: {}, usersPath: str, mediaButton: str, blogsButton: str, newsButton: str, inboxButton: str, dmButton: str, newDM: str, repliesButton: str, newReply: str, minimal: bool, sentButton: str, sharesButtonStr: str, bookmarksButtonStr: str, eventsButtonStr: str, moderationButtonStr: str, newPostButtonStr: str, baseDir: str, nickname: str, domain: str, iconsDir: str, timelineStartTime, newCalendarEvent: bool, calendarPath: str, calendarImage: str, followApprovals: str, iconsAsButtons: bool) -> str: """Returns the header at the top of the timeline, containing buttons for inbox, outbox, search, calendar, etc """ # start of the button header with inbox, outbox, etc tlStr = '
\n' # first button if defaultTimeline == 'tlmedia': tlStr += \ '' elif defaultTimeline == 'tlblogs': tlStr += \ '' elif defaultTimeline == 'tlnews': tlStr += \ '' else: tlStr += \ '' # if this is a news instance and we are viewing the news timeline newsHeader = False if defaultTimeline == 'tlnews' and boxName == 'tlnews': newsHeader = True if not newsHeader: tlStr += \ '' tlStr += \ '' # typically the media button if defaultTimeline != 'tlmedia': if not minimal and not newsHeader: tlStr += \ '' else: if not minimal: tlStr += \ '' isFeaturesTimeline = \ defaultTimeline == 'tlnews' and boxName == 'tlnews' if not isFeaturesTimeline: # typically the blogs button # but may change if this is a blogging oriented instance if defaultTimeline != 'tlblogs': if not minimal and not isFeaturesTimeline: titleStr = translate['Blogs'] if defaultTimeline == 'tlnews': titleStr = translate['Article'] tlStr += \ '' else: if not minimal: tlStr += \ '' # typically the news button # but may change if this is a news oriented instance if defaultTimeline != 'tlnews': tlStr += \ '' else: if not newsHeader: tlStr += \ '' # show todays events buttons on the first inbox page happeningStr = '' if boxName == 'inbox' and pageNumber == 1: if todaysEventsCheck(baseDir, nickname, domain): now = datetime.now() # happening today button if not iconsAsButtons: happeningStr += \ '' + \ '' else: happeningStr += \ '' + \ '' # happening this week button if thisWeeksEventsCheck(baseDir, nickname, domain): if not iconsAsButtons: happeningStr += \ '' else: happeningStr += \ '' else: # happening this week button if thisWeeksEventsCheck(baseDir, nickname, domain): if not iconsAsButtons: happeningStr += \ '' else: happeningStr += \ '' if not newsHeader: # button for the outbox tlStr += \ '' # add other buttons tlStr += \ sharesButtonStr + bookmarksButtonStr + eventsButtonStr + \ moderationButtonStr + happeningStr + newPostButtonStr if not newsHeader: if not iconsAsButtons: # the search icon tlStr += \ '| ' + \
                translate['Search and follow'] + \
                '' else: # the search button tlStr += \ '' # benchmark 5 timeDiff = int((time.time() - timelineStartTime) * 1000) if timeDiff > 100: print('TIMELINE TIMING ' + boxName + ' 5 = ' + str(timeDiff)) # the calendar button if not isFeaturesTimeline: calendarAltText = translate['Calendar'] if newCalendarEvent: # indicate that the calendar icon is highlighted calendarAltText = '*' + calendarAltText + '*' if not iconsAsButtons: tlStr += \ ' | ' + calendarAltText + \
                '\n' else: tlStr += \ '' if not newsHeader: # the show/hide button, for a simpler header appearance if not iconsAsButtons: tlStr += \ ' | ' + translate['Show/Hide Buttons'] + \
                '\n' else: tlStr += \ '' if newsHeader: tlStr += \ '' + \ '' # the newswire button to show right column links if not iconsAsButtons: tlStr += \ '' + \ '| ' + translate['News'] + \
            '' else: # NOTE: deliberately no \n at end of line tlStr += \ '' # the links button to show left column links if not iconsAsButtons: tlStr += \ '' + \ '| ' + translate['Edit Links'] + \
            '' # end of headericons div tlStr += '
' else: # NOTE: deliberately no \n at end of line tlStr += \ '' if newsHeader: tlStr += \ '' + \ '' if not newsHeader: tlStr += followApprovals # end of the button header with inbox, outbox, etc tlStr += '
\n' return tlStr def htmlTimeline(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, baseDir: str, wfRequest: {}, personCache: {}, nickname: str, domain: str, port: int, timelineJson: {}, boxName: str, allowDeletion: bool, httpPrefix: str, projectVersion: str, manuallyApproveFollowers: bool, minimal: bool, YTReplacementDomain: str, showPublishedDateOnly: bool, newswire: {}, moderator: bool, editor: bool, positiveVoting: bool, showPublishAsIcon: bool, fullWidthTimelineButtonHeader: bool, iconsAsButtons: bool, rssIconAtTop: bool, publishButtonAtTop: bool, authorized: bool) -> str: """Show the timeline as html """ timelineStartTime = time.time() accountDir = baseDir + '/accounts/' + nickname + '@' + domain # should the calendar icon be highlighted? newCalendarEvent = False calendarImage = 'calendar.png' calendarPath = '/calendar' calendarFile = accountDir + '/.newCalendar' if os.path.isfile(calendarFile): newCalendarEvent = True calendarImage = 'calendar_notify.png' with open(calendarFile, 'r') as calfile: calendarPath = calfile.read().replace('##sent##', '') calendarPath = calendarPath.replace('\n', '').replace('\r', '') # should the DM button be highlighted? newDM = False dmFile = accountDir + '/.newDM' if os.path.isfile(dmFile): newDM = True if boxName == 'dm': os.remove(dmFile) # should the Replies button be highlighted? newReply = False replyFile = accountDir + '/.newReply' if os.path.isfile(replyFile): newReply = True if boxName == 'tlreplies': os.remove(replyFile) # should the Shares button be highlighted? newShare = False newShareFile = accountDir + '/.newShare' if os.path.isfile(newShareFile): newShare = True if boxName == 'tlshares': os.remove(newShareFile) # should the Moderation/reports button be highlighted? newReport = False newReportFile = accountDir + '/.newReport' if os.path.isfile(newReportFile): newReport = True if boxName == 'moderation': os.remove(newReportFile) # directory where icons are found # This changes depending upon theme iconsDir = getIconsDir(baseDir) separatorStr = '' if boxName != 'tlmedia': separatorStr = htmlPostSeparator(baseDir, None) # the css filename cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' # filename of the banner shown at the top bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) # benchmark 1 timeDiff = int((time.time() - timelineStartTime) * 1000) if timeDiff > 100: print('TIMELINE TIMING ' + boxName + ' 1 = ' + str(timeDiff)) profileStyle = getCSS(baseDir, cssFilename, cssCache) if not profileStyle: print('ERROR: css file not found ' + cssFilename) return None # replace any https within the css with whatever prefix is needed if httpPrefix != 'https': profileStyle = \ profileStyle.replace('https://', httpPrefix + '://') # is the user a moderator? if not moderator: moderator = isModerator(baseDir, nickname) # is the user a site editor? if not editor: editor = isEditor(baseDir, nickname) # benchmark 2 timeDiff = int((time.time() - timelineStartTime) * 1000) if timeDiff > 100: print('TIMELINE TIMING ' + boxName + ' 2 = ' + str(timeDiff)) # the appearance of buttons - highlighted or not inboxButton = 'button' blogsButton = 'button' newsButton = 'button' dmButton = 'button' if newDM: dmButton = 'buttonhighlighted' repliesButton = 'button' if newReply: repliesButton = 'buttonhighlighted' mediaButton = 'button' bookmarksButton = 'button' eventsButton = 'button' sentButton = 'button' sharesButton = 'button' if newShare: sharesButton = 'buttonhighlighted' moderationButton = 'button' if newReport: moderationButton = 'buttonhighlighted' if boxName == 'inbox': inboxButton = 'buttonselected' elif boxName == 'tlblogs': blogsButton = 'buttonselected' elif boxName == 'tlnews': newsButton = 'buttonselected' elif boxName == 'dm': dmButton = 'buttonselected' if newDM: dmButton = 'buttonselectedhighlighted' elif boxName == 'tlreplies': repliesButton = 'buttonselected' if newReply: repliesButton = 'buttonselectedhighlighted' elif boxName == 'tlmedia': mediaButton = 'buttonselected' elif boxName == 'outbox': sentButton = 'buttonselected' elif boxName == 'moderation': moderationButton = 'buttonselected' if newReport: moderationButton = 'buttonselectedhighlighted' elif boxName == 'tlshares': sharesButton = 'buttonselected' if newShare: sharesButton = 'buttonselectedhighlighted' elif boxName == 'tlbookmarks' or boxName == 'bookmarks': bookmarksButton = 'buttonselected' elif boxName == 'tlevents': eventsButton = 'buttonselected' # get the full domain, including any port number fullDomain = domain if port != 80 and port != 443: if ':' not in domain: fullDomain = domain + ':' + str(port) usersPath = '/users/' + nickname actor = httpPrefix + '://' + fullDomain + usersPath showIndividualPostIcons = True # show an icon for new follow approvals followApprovals = '' followRequestsFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '/followrequests.txt' if os.path.isfile(followRequestsFilename): with open(followRequestsFilename, 'r') as f: for line in f: if len(line) > 0: # show follow approvals icon followApprovals = \ '' + \ '' + \
                        translate['Approve follow requests'] + \
                        '\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 = \ '' # shares, bookmarks and events buttons sharesButtonStr = '' bookmarksButtonStr = '' eventsButtonStr = '' if not minimal: sharesButtonStr = \ '' bookmarksButtonStr = \ '' eventsButtonStr = \ '' tlStr = htmlHeader(cssFilename, profileStyle) # benchmark 4 timeDiff = int((time.time() - timelineStartTime) * 1000) if timeDiff > 100: print('TIMELINE TIMING ' + boxName + ' 4 = ' + str(timeDiff)) # if this is a news instance and we are viewing the news timeline newsHeader = False if defaultTimeline == 'tlnews' and boxName == 'tlnews': newsHeader = True newPostButtonStr = '' # start of headericons div if not newsHeader: if not iconsAsButtons: newPostButtonStr += '
' # what screen to go to when a new post is created if boxName == 'dm': if not iconsAsButtons: newPostButtonStr += \ '| ' + translate['Create a new DM'] + \
                '\n' else: newPostButtonStr += \ '' + \ '' elif boxName == 'tlblogs' or boxName == 'tlnews': if not iconsAsButtons: newPostButtonStr += \ '| ' + \
                translate['Create a new post'] + \
                '\n' else: newPostButtonStr += \ '' + \ '' elif boxName == 'tlevents': if not iconsAsButtons: newPostButtonStr += \ '| ' + \
                translate['Create a new event'] + \
                '\n' else: newPostButtonStr += \ '' + \ '' else: if not manuallyApproveFollowers: if not iconsAsButtons: newPostButtonStr += \ '| ' + \
                    translate['Create a new post'] + \
                    '\n' else: newPostButtonStr += \ '' + \ '' else: if not iconsAsButtons: newPostButtonStr += \ '| ' + translate['Create a new post'] + \
                    '\n' else: newPostButtonStr += \ '' + \ '' # This creates a link to the profile page when viewed # in lynx, but should be invisible in a graphical web browser tlStr += \ '\n' # banner and row of buttons tlStr += \ '\n' tlStr += '\n' if fullWidthTimelineButtonHeader: tlStr += \ headerButtonsTimeline(defaultTimeline, boxName, pageNumber, translate, usersPath, mediaButton, blogsButton, newsButton, inboxButton, dmButton, newDM, repliesButton, newReply, minimal, sentButton, sharesButtonStr, bookmarksButtonStr, eventsButtonStr, moderationButtonStr, newPostButtonStr, baseDir, nickname, domain, iconsDir, timelineStartTime, newCalendarEvent, calendarPath, calendarImage, followApprovals, iconsAsButtons) # 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, editor, False, None, rssIconAtTop, True, False) tlStr += ' \n' # center column containing posts tlStr += ' \n' # right column rightColumnStr = getRightColumnContent(baseDir, nickname, domainFull, httpPrefix, translate, iconsDir, moderator, editor, newswire, positiveVoting, False, None, True, showPublishAsIcon, rssIconAtTop, publishButtonAtTop, authorized, True) tlStr += ' \n' tlStr += ' \n' # benchmark 9 timeDiff = int((time.time() - timelineStartTime) * 1000) if timeDiff > 100: print('TIMELINE TIMING ' + boxName + ' 9 = ' + str(timeDiff)) tlStr += ' \n' tlStr += '
' + \ leftColumnStr + ' \n' if not fullWidthTimelineButtonHeader: tlStr += \ headerButtonsTimeline(defaultTimeline, boxName, pageNumber, translate, usersPath, mediaButton, blogsButton, newsButton, inboxButton, dmButton, newDM, repliesButton, newReply, minimal, sentButton, sharesButtonStr, bookmarksButtonStr, eventsButtonStr, moderationButtonStr, newPostButtonStr, baseDir, nickname, domain, iconsDir, timelineStartTime, newCalendarEvent, calendarPath, calendarImage, followApprovals, iconsAsButtons) # 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' + \ ' ' + \
            translate['Page up'] + '\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, showPublishedDateOnly, 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 if separatorStr: tlStr += separatorStr tlStr += currTlStr if boxName == 'tlmedia': tlStr += '
\n' # page down arrow if itemCtr > 2: tlStr += \ '
\n' + \ ' ' + \
            translate['Page down'] + '\n' + \ '
\n' # end of column-center tlStr += '
' + \ rightColumnStr + '
\n' tlStr += htmlFooter() return tlStr def htmlShares(cssCache: {}, 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, showPublishedDateOnly: bool, newswire: {}, positiveVoting: bool, showPublishAsIcon: bool, fullWidthTimelineButtonHeader: bool, iconsAsButtons: bool, rssIconAtTop: bool, publishButtonAtTop: bool, authorized: bool) -> str: """Show the shares timeline as html """ manuallyApproveFollowers = \ followerApprovalActive(baseDir, nickname, domain) return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, None, 'tlshares', allowDeletion, httpPrefix, projectVersion, manuallyApproveFollowers, False, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized) def htmlInbox(cssCache: {}, 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, showPublishedDateOnly: bool, newswire: {}, positiveVoting: bool, showPublishAsIcon: bool, fullWidthTimelineButtonHeader: bool, iconsAsButtons: bool, rssIconAtTop: bool, publishButtonAtTop: bool, authorized: bool) -> str: """Show the inbox as html """ manuallyApproveFollowers = \ followerApprovalActive(baseDir, nickname, domain) return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'inbox', allowDeletion, httpPrefix, projectVersion, manuallyApproveFollowers, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized) def htmlBookmarks(cssCache: {}, 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, showPublishedDateOnly: bool, newswire: {}, positiveVoting: bool, showPublishAsIcon: bool, fullWidthTimelineButtonHeader: bool, iconsAsButtons: bool, rssIconAtTop: bool, publishButtonAtTop: bool, authorized: bool) -> str: """Show the bookmarks as html """ manuallyApproveFollowers = \ followerApprovalActive(baseDir, nickname, domain) return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, bookmarksJson, 'tlbookmarks', allowDeletion, httpPrefix, projectVersion, manuallyApproveFollowers, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized) def htmlEvents(cssCache: {}, 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, showPublishedDateOnly: bool, newswire: {}, positiveVoting: bool, showPublishAsIcon: bool, fullWidthTimelineButtonHeader: bool, iconsAsButtons: bool, rssIconAtTop: bool, publishButtonAtTop: bool, authorized: bool) -> str: """Show the events as html """ manuallyApproveFollowers = \ followerApprovalActive(baseDir, nickname, domain) return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, bookmarksJson, 'tlevents', allowDeletion, httpPrefix, projectVersion, manuallyApproveFollowers, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized) def htmlInboxDMs(cssCache: {}, 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, showPublishedDateOnly: bool, newswire: {}, positiveVoting: bool, showPublishAsIcon: bool, fullWidthTimelineButtonHeader: bool, iconsAsButtons: bool, rssIconAtTop: bool, publishButtonAtTop: bool, authorized: bool) -> str: """Show the DM timeline as html """ return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'dm', allowDeletion, httpPrefix, projectVersion, False, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized) def htmlInboxReplies(cssCache: {}, 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, showPublishedDateOnly: bool, newswire: {}, positiveVoting: bool, showPublishAsIcon: bool, fullWidthTimelineButtonHeader: bool, iconsAsButtons: bool, rssIconAtTop: bool, publishButtonAtTop: bool, authorized: bool) -> str: """Show the replies timeline as html """ return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'tlreplies', allowDeletion, httpPrefix, projectVersion, False, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized) def htmlInboxMedia(cssCache: {}, 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, showPublishedDateOnly: bool, newswire: {}, positiveVoting: bool, showPublishAsIcon: bool, fullWidthTimelineButtonHeader: bool, iconsAsButtons: bool, rssIconAtTop: bool, publishButtonAtTop: bool, authorized: bool) -> str: """Show the media timeline as html """ return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'tlmedia', allowDeletion, httpPrefix, projectVersion, False, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized) def htmlInboxBlogs(cssCache: {}, 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, showPublishedDateOnly: bool, newswire: {}, positiveVoting: bool, showPublishAsIcon: bool, fullWidthTimelineButtonHeader: bool, iconsAsButtons: bool, rssIconAtTop: bool, publishButtonAtTop: bool, authorized: bool) -> str: """Show the blogs timeline as html """ return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'tlblogs', allowDeletion, httpPrefix, projectVersion, False, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized) def htmlInboxNews(cssCache: {}, 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, showPublishedDateOnly: bool, newswire: {}, moderator: bool, editor: bool, positiveVoting: bool, showPublishAsIcon: bool, fullWidthTimelineButtonHeader: bool, iconsAsButtons: bool, rssIconAtTop: bool, publishButtonAtTop: bool, authorized: bool) -> str: """Show the news timeline as html """ return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'tlnews', allowDeletion, httpPrefix, projectVersion, False, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, moderator, editor, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized) def htmlModeration(cssCache: {}, 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, showPublishedDateOnly: bool, newswire: {}, positiveVoting: bool, showPublishAsIcon: bool, fullWidthTimelineButtonHeader: bool, iconsAsButtons: bool, rssIconAtTop: bool, publishButtonAtTop: bool, authorized: bool) -> str: """Show the moderation feed as html """ return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'moderation', allowDeletion, httpPrefix, projectVersion, True, False, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized) def htmlOutbox(cssCache: {}, 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, showPublishedDateOnly: bool, newswire: {}, positiveVoting: bool, showPublishAsIcon: bool, fullWidthTimelineButtonHeader: bool, iconsAsButtons: bool, rssIconAtTop: bool, publishButtonAtTop: bool, authorized: bool) -> str: """Show the Outbox as html """ manuallyApproveFollowers = \ followerApprovalActive(baseDir, nickname, domain) return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, maxRecentPosts, translate, pageNumber, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, outboxJson, 'outbox', allowDeletion, httpPrefix, projectVersion, manuallyApproveFollowers, minimal, YTReplacementDomain, showPublishedDateOnly, newswire, False, False, positiveVoting, showPublishAsIcon, fullWidthTimelineButtonHeader, iconsAsButtons, rssIconAtTop, publishButtonAtTop, authorized) def htmlIndividualPost(cssCache: {}, recentPostsCache: {}, maxRecentPosts: int, translate: {}, baseDir: str, session, wfRequest: {}, personCache: {}, nickname: str, domain: str, port: int, authorized: bool, postJsonObject: {}, httpPrefix: str, projectVersion: str, likedBy: str, YTReplacementDomain: str, showPublishedDateOnly: bool) -> str: """Show an individual post as html """ iconsDir = getIconsDir(baseDir) postStr = '' if likedBy: likedByNickname = getNicknameFromActor(likedBy) likedByDomain, likedByPort = getDomainFromActor(likedBy) if likedByPort: if likedByPort != 80 and likedByPort != 443: likedByDomain += ':' + str(likedByPort) likedByHandle = likedByNickname + '@' + likedByDomain postStr += \ '

' + translate['Liked by'] + \ ' @' + \ likedByHandle + '\n' domainFull = domain if port: if port != 80 and port != 443: domainFull = domain + ':' + str(port) actor = '/users/' + nickname followStr = '

\n' followStr += \ ' \n' followStr += \ ' \n' if not isFollowingActor(baseDir, nickname, domainFull, likedBy): followStr += ' \n' followStr += ' \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, showPublishedDateOnly, False, authorized, False, False, False) messageId = removeIdEnding(postJsonObject['id']) # show the previous posts if isinstance(postJsonObject['object'], dict): while postJsonObject['object'].get('inReplyTo'): postFilename = \ locatePost(baseDir, nickname, domain, postJsonObject['object']['inReplyTo']) if not postFilename: break postJsonObject = loadJson(postFilename) if postJsonObject: postStr = \ individualPostAsHtml(True, recentPostsCache, maxRecentPosts, iconsDir, translate, None, baseDir, session, wfRequest, personCache, nickname, domain, port, postJsonObject, None, True, False, httpPrefix, projectVersion, 'inbox', YTReplacementDomain, showPublishedDateOnly, False, authorized, False, False, False) + postStr # show the following posts postFilename = locatePost(baseDir, nickname, domain, messageId) if postFilename: # is there a replies file for this post? repliesFilename = postFilename.replace('.json', '.replies') if os.path.isfile(repliesFilename): # get items from the replies file repliesJson = { 'orderedItems': [] } populateRepliesJson(baseDir, nickname, domain, repliesFilename, authorized, repliesJson) # add items to the html output for item in repliesJson['orderedItems']: postStr += \ individualPostAsHtml(True, recentPostsCache, maxRecentPosts, iconsDir, translate, None, baseDir, session, wfRequest, personCache, nickname, domain, port, item, None, True, False, httpPrefix, projectVersion, 'inbox', YTReplacementDomain, showPublishedDateOnly, False, authorized, False, False, False) cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' postsCSS = getCSS(baseDir, cssFilename, cssCache) if postsCSS: if httpPrefix != 'https': postsCSS = postsCSS.replace('https://', httpPrefix + '://') return htmlHeader(cssFilename, postsCSS) + postStr + htmlFooter() def htmlPostReplies(cssCache: {}, recentPostsCache: {}, maxRecentPosts: int, translate: {}, baseDir: str, session, wfRequest: {}, personCache: {}, nickname: str, domain: str, port: int, repliesJson: {}, httpPrefix: str, projectVersion: str, YTReplacementDomain: str, showPublishedDateOnly: bool) -> str: """Show the replies to an individual post as html """ iconsDir = getIconsDir(baseDir) repliesStr = '' if repliesJson.get('orderedItems'): for item in repliesJson['orderedItems']: repliesStr += \ individualPostAsHtml(True, recentPostsCache, maxRecentPosts, iconsDir, translate, None, baseDir, session, wfRequest, personCache, nickname, domain, port, item, None, True, False, httpPrefix, projectVersion, 'inbox', YTReplacementDomain, showPublishedDateOnly, False, False, False, False, False) cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' postsCSS = getCSS(baseDir, cssFilename, cssCache) if postsCSS: if httpPrefix != 'https': postsCSS = postsCSS.replace('https://', httpPrefix + '://') return htmlHeader(cssFilename, postsCSS) + repliesStr + htmlFooter() def htmlRemoveSharedItem(cssCache: {}, 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' profileStyle = getCSS(baseDir, cssFilename, cssCache) sharesStr = htmlHeader(cssFilename, profileStyle) sharesStr += '\n' sharesStr += htmlFooter() return sharesStr def htmlDeletePost(cssCache: {}, recentPostsCache: {}, maxRecentPosts: int, translate, pageNumber: int, session, baseDir: str, messageId: str, httpPrefix: str, projectVersion: str, wfRequest: {}, personCache: {}, callingDomain: str, YTReplacementDomain: str, showPublishedDateOnly: bool) -> 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' profileStyle = getCSS(baseDir, cssFilename, cssCache) if profileStyle: 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, showPublishedDateOnly, False, False, False, False, False) deletePostStr += '
' deletePostStr += \ '

' + \ translate['Delete this post?'] + '

' postActor = getAltPath(actor, domainFull, callingDomain) deletePostStr += \ '
\n' deletePostStr += \ ' \n' deletePostStr += \ ' \n' deletePostStr += \ ' \n' deletePostStr += \ ' \n' deletePostStr += '
\n' deletePostStr += '
\n' deletePostStr += htmlFooter() return deletePostStr def htmlFollowConfirm(cssCache: {}, 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' profileStyle = getCSS(baseDir, cssFilename, cssCache) followStr = htmlHeader(cssFilename, profileStyle) followStr += '\n' followStr += htmlFooter() return followStr def htmlUnfollowConfirm(cssCache: {}, 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' profileStyle = getCSS(baseDir, cssFilename, cssCache) followStr = htmlHeader(cssFilename, profileStyle) followStr += '\n' followStr += htmlFooter() return followStr def htmlPersonOptions(cssCache: {}, translate: {}, baseDir: str, domain: str, domainFull: str, originPathStr: str, optionsActor: str, optionsProfileUrl: str, optionsLink: str, pageNumber: int, donateUrl: str, xmppAddress: str, matrixAddress: str, ssbAddress: str, blogAddress: str, toxAddress: str, PGPpubKey: str, PGPfingerprint: str, emailAddress) -> str: """Show options for a person: view/follow/block/report """ optionsDomain, optionsPort = getDomainFromActor(optionsActor) optionsDomainFull = optionsDomain if optionsPort: if optionsPort != 80 and optionsPort != 443: optionsDomainFull = optionsDomain + ':' + str(optionsPort) if os.path.isfile(baseDir + '/accounts/options-background-custom.jpg'): if not os.path.isfile(baseDir + '/accounts/options-background.jpg'): copyfile(baseDir + '/accounts/options-background.jpg', baseDir + '/accounts/options-background.jpg') followStr = 'Follow' blockStr = 'Block' nickname = None optionsNickname = None if originPathStr.startswith('/users/'): nickname = originPathStr.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] if '?' in nickname: nickname = nickname.split('?')[0] followerDomain, followerPort = getDomainFromActor(optionsActor) if isFollowingActor(baseDir, nickname, domain, optionsActor): followStr = 'Unfollow' optionsNickname = getNicknameFromActor(optionsActor) optionsDomainFull = optionsDomain if optionsPort: if optionsPort != 80 and optionsPort != 443: optionsDomainFull = optionsDomain + ':' + str(optionsPort) if isBlocked(baseDir, nickname, domain, optionsNickname, optionsDomainFull): blockStr = 'Block' optionsLinkStr = '' if optionsLink: optionsLinkStr = \ ' \n' cssFilename = baseDir + '/epicyon-options.css' if os.path.isfile(baseDir + '/options.css'): cssFilename = baseDir + '/options.css' profileStyle = getCSS(baseDir, cssFilename, cssCache) if profileStyle: profileStyle = \ profileStyle.replace('--follow-text-entry-width: 90%;', '--follow-text-entry-width: 20%;') if not os.path.isfile(baseDir + '/accounts/' + 'options-background.jpg'): profileStyle = \ profileStyle.replace('background-image: ' + 'url("options-background.jpg");', 'background-image: none;') # To snooze, or not to snooze? That is the question snoozeButtonStr = 'Snooze' if nickname: if isPersonSnoozed(baseDir, nickname, domain, optionsActor): snoozeButtonStr = 'Unsnooze' donateStr = '' if donateUrl: donateStr = \ ' \n' optionsStr = htmlHeader(cssFilename, profileStyle) optionsStr += '

\n' optionsStr += '
\n' optionsStr += '
\n' optionsStr += '
\n' optionsStr += ' \n' optionsStr += ' \n' handle = getNicknameFromActor(optionsActor) + '@' + optionsDomain optionsStr += \ '

' + translate['Options for'] + \ ' @' + handle + '

\n' if emailAddress: optionsStr += \ '

' + translate['Email'] + \ ': ' + emailAddress + '

\n' if xmppAddress: optionsStr += \ '

' + translate['XMPP'] + \ ': ' + \ xmppAddress + '

\n' if matrixAddress: optionsStr += \ '

' + translate['Matrix'] + ': ' + \ matrixAddress + '

\n' if ssbAddress: optionsStr += \ '

SSB: ' + ssbAddress + '

\n' if blogAddress: optionsStr += \ '

Blog: ' + \ blogAddress + '

\n' if toxAddress: optionsStr += \ '

Tox: ' + toxAddress + '

\n' if PGPfingerprint: optionsStr += '

PGP: ' + \ PGPfingerprint.replace('\n', '
') + '

\n' if PGPpubKey: optionsStr += '

' + \ PGPpubKey.replace('\n', '
') + '

\n' optionsStr += '
\n' optionsStr += ' \n' optionsStr += ' \n' optionsStr += ' \n' if optionsNickname: handle = optionsNickname + '@' + optionsDomainFull petname = getPetName(baseDir, nickname, domain, handle) optionsStr += \ ' ' + translate['Petname'] + ': \n' + \ ' \n' \ '
\n' # checkbox for receiving calendar events if isFollowingActor(baseDir, nickname, domain, optionsActor): checkboxStr = \ ' ' + \ translate['Receive calendar events from this account'] + \ '\n
\n' if not receivingCalendarEvents(baseDir, nickname, domain, optionsNickname, optionsDomainFull): checkboxStr = checkboxStr.replace(' checked>', '>') optionsStr += checkboxStr # checkbox for permission to post to newswire if optionsDomainFull == domainFull: if isModerator(baseDir, nickname) and \ not isModerator(baseDir, optionsNickname): newswireBlockedFilename = \ baseDir + '/accounts/' + \ optionsNickname + '@' + optionsDomain + '/.nonewswire' checkboxStr = \ ' ' + \ translate['Allow news posts'] + \ '\n
\n' if os.path.isfile(newswireBlockedFilename): checkboxStr = checkboxStr.replace(' checked>', '>') optionsStr += checkboxStr optionsStr += optionsLinkStr optionsStr += \ ' ' optionsStr += \ ' ' optionsStr += donateStr optionsStr += \ ' ' optionsStr += \ ' ' optionsStr += \ ' ' optionsStr += \ ' ' optionsStr += \ ' ' 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 += '
\n' optionsStr += \ ' \n' optionsStr += '
\n' optionsStr += '
\n' optionsStr += '
\n' optionsStr += '
\n' optionsStr += htmlFooter() return optionsStr def htmlUnblockConfirm(cssCache: {}, 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' profileStyle = getCSS(baseDir, cssFilename, cssCache) 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 += \ ' \n' blockStr += \ ' \n' blockStr += '
\n' blockStr += '
\n' blockStr += '
\n' blockStr += '
\n' blockStr += htmlFooter() return blockStr def htmlProfileAfterSearch(cssCache: {}, recentPostsCache: {}, maxRecentPosts: int, translate: {}, baseDir: str, path: str, httpPrefix: str, nickname: str, domain: str, port: int, profileHandle: str, session, cachedWebfingers: {}, personCache: {}, debug: bool, projectVersion: str, YTReplacementDomain: str, showPublishedDateOnly: bool) -> str: """Show a profile page after a search for a fediverse address """ if '/users/' in profileHandle or \ '/accounts/' in profileHandle or \ '/channel/' in profileHandle or \ '/profile/' in profileHandle or \ '/@' in profileHandle: searchNickname = getNicknameFromActor(profileHandle) searchDomain, searchPort = getDomainFromActor(profileHandle) else: if '@' not in profileHandle: print('DEBUG: no @ in ' + profileHandle) return None if profileHandle.startswith('@'): profileHandle = profileHandle[1:] if '@' not in profileHandle: print('DEBUG: no @ in ' + profileHandle) return None searchNickname = profileHandle.split('@')[0] searchDomain = profileHandle.split('@')[1] searchPort = None if ':' in searchDomain: searchPortStr = searchDomain.split(':')[1] if searchPortStr.isdigit(): searchPort = int(searchPortStr) searchDomain = searchDomain.split(':')[0] if searchPort: print('DEBUG: Search for handle ' + str(searchNickname) + '@' + str(searchDomain) + ':' + str(searchPort)) else: print('DEBUG: Search for handle ' + str(searchNickname) + '@' + str(searchDomain)) if not searchNickname: print('DEBUG: No nickname found in ' + profileHandle) return None if not searchDomain: print('DEBUG: No domain found in ' + profileHandle) return None searchDomainFull = searchDomain if searchPort: if searchPort != 80 and searchPort != 443: if ':' not in searchDomain: searchDomainFull = searchDomain + ':' + str(searchPort) profileStr = '' cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' profileStyle = getCSS(baseDir, cssFilename, cssCache) if profileStyle: wf = \ webfingerHandle(session, searchNickname + '@' + searchDomainFull, httpPrefix, cachedWebfingers, domain, projectVersion) if not wf: print('DEBUG: Unable to webfinger ' + searchNickname + '@' + searchDomainFull) print('DEBUG: cachedWebfingers ' + str(cachedWebfingers)) print('DEBUG: httpPrefix ' + httpPrefix) print('DEBUG: domain ' + domain) return None if not isinstance(wf, dict): print('WARN: Webfinger search for ' + searchNickname + '@' + searchDomainFull + ' did not return a dict. ' + str(wf)) return None personUrl = None if wf.get('errors'): personUrl = httpPrefix + '://' + \ searchDomainFull + '/users/' + searchNickname profileStr = 'https://www.w3.org/ns/activitystreams' asHeader = { 'Accept': 'application/activity+json; profile="' + profileStr + '"' } if not personUrl: personUrl = getUserUrl(wf) if not personUrl: # try single user instance asHeader = { 'Accept': 'application/ld+json; profile="' + profileStr + '"' } personUrl = httpPrefix + '://' + searchDomainFull profileJson = \ getJson(session, personUrl, asHeader, None, projectVersion, httpPrefix, domain) if not profileJson: asHeader = { 'Accept': 'application/ld+json; profile="' + profileStr + '"' } profileJson = \ getJson(session, personUrl, asHeader, None, projectVersion, httpPrefix, domain) if not profileJson: print('DEBUG: No actor returned from ' + personUrl) return None avatarUrl = '' if profileJson.get('icon'): if profileJson['icon'].get('url'): avatarUrl = profileJson['icon']['url'] if not avatarUrl: avatarUrl = getPersonAvatarUrl(baseDir, personUrl, personCache, True) displayName = searchNickname if profileJson.get('name'): displayName = profileJson['name'] profileDescription = '' if profileJson.get('summary'): profileDescription = profileJson['summary'] outboxUrl = None if not profileJson.get('outbox'): if debug: pprint(profileJson) print('DEBUG: No outbox found') return None outboxUrl = profileJson['outbox'] profileBackgroundImage = '' if profileJson.get('image'): if profileJson['image'].get('url'): profileBackgroundImage = profileJson['image']['url'] profileStyle = profileStyle.replace('image.png', profileBackgroundImage) if httpPrefix != 'https': profileStyle = profileStyle.replace('https://', httpPrefix + '://') # url to return to backUrl = path if not backUrl.endswith('/inbox'): backUrl += '/inbox' profileDescriptionShort = profileDescription if '\n' in profileDescription: if len(profileDescription.split('\n')) > 2: profileDescriptionShort = '' else: if '
' in profileDescription: if len(profileDescription.split('
')) > 2: profileDescriptionShort = '' # keep the profile description short if len(profileDescriptionShort) > 256: profileDescriptionShort = '' # remove formatting from profile description used on title avatarDescription = '' if profileJson.get('summary'): if isinstance(profileJson['summary'], str): avatarDescription = \ profileJson['summary'].replace('
', '\n') avatarDescription = avatarDescription.replace('

', '') avatarDescription = avatarDescription.replace('

', '') if '<' in avatarDescription: avatarDescription = removeHtml(avatarDescription) profileStr = '
\n' profileStr += '
\n' if avatarUrl: profileStr += \ ' ' + avatarDescription + '\n' profileStr += '

' + displayName + '

\n' profileStr += '

@' + searchNickname + '@' + \ searchDomainFull + '

\n' profileStr += '

' + profileDescriptionShort + '

\n' profileStr += '
\n' profileStr += '
\n' profileStr += '
\n' profileStr += '
\n' profileStr += '
\n' profileStr += \ ' \n' profileStr += \ ' \n' profileStr += \ ' \n' profileStr += \ ' \n' profileStr += '
\n' profileStr += '
\n' profileStr += '
\n' iconsDir = getIconsDir(baseDir) i = 0 for item in parseUserFeed(session, outboxUrl, asHeader, projectVersion, httpPrefix, domain): if not item.get('type'): continue if item['type'] != 'Create' and item['type'] != 'Announce': continue if not item.get('object'): continue profileStr += \ individualPostAsHtml(True, recentPostsCache, maxRecentPosts, iconsDir, translate, None, baseDir, session, cachedWebfingers, personCache, nickname, domain, port, item, avatarUrl, False, False, httpPrefix, projectVersion, 'inbox', YTReplacementDomain, showPublishedDateOnly, False, False, False, False, False) i += 1 if i >= 20: break return htmlHeader(cssFilename, profileStyle) + profileStr + htmlFooter()