__filename__ = "webinterface.py" __author__ = "Bob Mottram" __license__ = "AGPL3+" __version__ = "1.1.0" __maintainer__ = "Bob Mottram" __email__ = "bob@freedombone.net" __status__ = "Production" import time import os import urllib.parse from collections import OrderedDict from datetime import datetime from datetime import date from dateutil.parser import parse 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 removeIdEnding from utils import getProtocolPrefixes from utils import searchBoxPosts from utils import isEventPost from utils import isBlogPost from utils import isNewsPost from utils import updateRecentPostsCache from utils import getNicknameFromActor from utils import getDomainFromActor from utils import locatePost from utils import noOfAccounts from utils import isPublicPost from utils import isPublicPostFromUrl from utils import getDisplayName from utils import getCachedPostDirectory from utils import getCachedPostFilename from utils import loadJson from utils import getConfigParam from utils import votesOnNewswireItem from follow import isFollowingActor from webfinger import webfingerHandle from posts import isDM 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 posts import downloadAnnounce from session import getJson from auth import createPassword from like import likedByPerson from like import noOfLikes from bookmarks import bookmarkedByPerson from announce import announcedByPerson from blocking import isBlocked from blocking import isBlockedHashtag from content import htmlReplaceEmailQuote from content import htmlReplaceQuoteMarks from content import removeTextFormatting from content import switchWords from content import getMentionsFromHtml from content import addHtmlTags from content import replaceEmojiFromTags from content import removeLongWords from content import removeHtml from skills import getSkills from cache import getPersonFromCache from cache import storePersonInCache from shares import getValidSharedItemID from happening import todaysEventsCheck from happening import thisWeeksEventsCheck from happening import getCalendarEvents from happening import getTodaysEvents from git import isGitPatch from theme import getThemesList from petnames import getPetName from followingCalendar import receivingCalendarEvents from devices import E2EEdecryptMessageFromDevice def getAltPath(actor: str, domainFull: str, callingDomain: str) -> str: """Returns alternate path from the actor eg. https://clearnetdomain/path becomes http://oniondomain/path """ postActor = actor if callingDomain not in actor and domainFull in actor: if callingDomain.endswith('.onion') or \ callingDomain.endswith('.i2p'): postActor = \ 'http://' + callingDomain + actor.split(domainFull)[1] print('Changed POST domain from ' + actor + ' to ' + postActor) return postActor def getContentWarningButton(postID: str, translate: {}, content: str) -> str: """Returns the markup for a content warning button """ return '
' + \ translate['SHOW MORE'] + '' + \ '
' + content + \ '
\n' def getBlogAddress(actorJson: {}) -> str: """Returns blog address for the given actor """ if not actorJson.get('attachment'): return '' for propertyValue in actorJson['attachment']: if not propertyValue.get('name'): continue if not propertyValue['name'].lower().startswith('blog'): continue if not propertyValue.get('type'): continue if not propertyValue.get('value'): continue if propertyValue['type'] != 'PropertyValue': continue propertyValue['value'] = propertyValue['value'].strip() prefixes = getProtocolPrefixes() prefixFound = False for prefix in prefixes: if propertyValue['value'].startswith(prefix): prefixFound = True break if not prefixFound: continue if '.' not in propertyValue['value']: continue if ' ' in propertyValue['value']: continue if ',' in propertyValue['value']: continue return propertyValue['value'] return '' def setBlogAddress(actorJson: {}, blogAddress: str) -> None: """Sets an blog address for the given actor """ if not actorJson.get('attachment'): actorJson['attachment'] = [] # remove any existing value propertyFound = None for propertyValue in actorJson['attachment']: if not propertyValue.get('name'): continue if not propertyValue.get('type'): continue if not propertyValue['name'].lower().startswith('blog'): continue propertyFound = propertyValue break if propertyFound: actorJson['attachment'].remove(propertyFound) prefixes = getProtocolPrefixes() prefixFound = False for prefix in prefixes: if blogAddress.startswith(prefix): prefixFound = True break if not prefixFound: return if '.' not in blogAddress: return if ' ' in blogAddress: return if ',' in blogAddress: return for propertyValue in actorJson['attachment']: if not propertyValue.get('name'): continue if not propertyValue.get('type'): continue if not propertyValue['name'].lower().startswith('blog'): continue if propertyValue['type'] != 'PropertyValue': continue propertyValue['value'] = blogAddress return newBlogAddress = { "name": "Blog", "type": "PropertyValue", "value": blogAddress } actorJson['attachment'].append(newBlogAddress) def updateAvatarImageCache(session, baseDir: str, httpPrefix: str, actor: str, avatarUrl: str, personCache: {}, allowDownloads: bool, force=False) -> str: """Updates the cached avatar for the given actor """ if not avatarUrl: return None actorStr = actor.replace('/', '-') avatarImagePath = baseDir + '/cache/avatars/' + actorStr if avatarUrl.endswith('.png') or \ '.png?' in avatarUrl: sessionHeaders = { 'Accept': 'image/png' } avatarImageFilename = avatarImagePath + '.png' elif (avatarUrl.endswith('.jpg') or avatarUrl.endswith('.jpeg') or '.jpg?' in avatarUrl or '.jpeg?' in avatarUrl): sessionHeaders = { 'Accept': 'image/jpeg' } avatarImageFilename = avatarImagePath + '.jpg' elif avatarUrl.endswith('.gif') or '.gif?' in avatarUrl: sessionHeaders = { 'Accept': 'image/gif' } avatarImageFilename = avatarImagePath + '.gif' elif avatarUrl.endswith('.webp') or '.webp?' in avatarUrl: sessionHeaders = { 'Accept': 'image/webp' } avatarImageFilename = avatarImagePath + '.webp' elif avatarUrl.endswith('.avif') or '.avif?' in avatarUrl: sessionHeaders = { 'Accept': 'image/avif' } avatarImageFilename = avatarImagePath + '.avif' else: return None if (not os.path.isfile(avatarImageFilename) or force) and allowDownloads: try: print('avatar image url: ' + avatarUrl) result = session.get(avatarUrl, headers=sessionHeaders, params=None) if result.status_code < 200 or \ result.status_code > 202: print('Avatar image download failed with status ' + str(result.status_code)) # remove partial download if os.path.isfile(avatarImageFilename): os.remove(avatarImageFilename) else: with open(avatarImageFilename, 'wb') as f: f.write(result.content) print('avatar image downloaded for ' + actor) return avatarImageFilename.replace(baseDir + '/cache', '') except Exception as e: print('Failed to download avatar image: ' + str(avatarUrl)) print(e) prof = 'https://www.w3.org/ns/activitystreams' if '/channel/' not in actor or '/accounts/' not in actor: sessionHeaders = { 'Accept': 'application/activity+json; profile="' + prof + '"' } else: sessionHeaders = { 'Accept': 'application/ld+json; profile="' + prof + '"' } personJson = \ getJson(session, actor, sessionHeaders, None, __version__, httpPrefix, None) if personJson: if not personJson.get('id'): return None if not personJson.get('publicKey'): return None if not personJson['publicKey'].get('publicKeyPem'): return None if personJson['id'] != actor: return None if not personCache.get(actor): return None if personCache[actor]['actor']['publicKey']['publicKeyPem'] != \ personJson['publicKey']['publicKeyPem']: print("ERROR: " + "public keys don't match when downloading actor for " + actor) return None storePersonInCache(baseDir, actor, personJson, personCache, allowDownloads) return getPersonAvatarUrl(baseDir, actor, personCache, allowDownloads) return None return avatarImageFilename.replace(baseDir + '/cache', '') def getPersonAvatarUrl(baseDir: str, personUrl: str, personCache: {}, allowDownloads: bool) -> str: """Returns the avatar url for the person """ personJson = \ getPersonFromCache(baseDir, personUrl, personCache, allowDownloads) if not personJson: return None # get from locally stored image actorStr = personJson['id'].replace('/', '-') avatarImagePath = baseDir + '/cache/avatars/' + actorStr imageExtension = ('png', 'jpg', 'jpeg', 'gif', 'webp', 'avif') for ext in imageExtension: if os.path.isfile(avatarImagePath + '.' + ext): return '/avatars/' + actorStr + '.' + ext elif os.path.isfile(avatarImagePath.lower() + '.' + ext): return '/avatars/' + actorStr.lower() + '.' + ext if personJson.get('icon'): if personJson['icon'].get('url'): return personJson['icon']['url'] return None def htmlFollowingList(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' with open(cssFilename, 'r') as cssFile: profileCSS = cssFile.read() 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 htmlSearchEmoji(translate: {}, baseDir: str, httpPrefix: str, searchStr: str) -> str: """Search results for emoji """ # emoji.json is generated so that it can be customized and the changes # will be retained even if default_emoji.json is subsequently updated if not os.path.isfile(baseDir + '/emoji/emoji.json'): copyfile(baseDir + '/emoji/default_emoji.json', baseDir + '/emoji/emoji.json') searchStr = searchStr.lower().replace(':', '').strip('\n').strip('\r') cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' with open(cssFilename, 'r') as cssFile: emojiCSS = cssFile.read() if httpPrefix != 'https': emojiCSS = emojiCSS.replace('https://', httpPrefix + '://') emojiLookupFilename = baseDir + '/emoji/emoji.json' # create header emojiForm = htmlHeader(cssFilename, emojiCSS) emojiForm += '

' + \ translate['Emoji Search'] + \ '

' # does the lookup file exist? if not os.path.isfile(emojiLookupFilename): emojiForm += '
' + \ translate['No results'] + '
' emojiForm += htmlFooter() return emojiForm emojiJson = loadJson(emojiLookupFilename) if emojiJson: results = {} for emojiName, filename in emojiJson.items(): if searchStr in emojiName: results[emojiName] = filename + '.png' for emojiName, filename in emojiJson.items(): if emojiName in searchStr: results[emojiName] = filename + '.png' headingShown = False emojiForm += '
' msgStr1 = translate['Copy the text then paste it into your post'] msgStr2 = ':' emojiForm += '
' emojiForm += htmlFooter() return emojiForm def getIconsDir(baseDir: str) -> str: """Returns the directory where icons exist """ iconsDir = 'icons' theme = getConfigParam(baseDir, 'theme') if theme: if os.path.isdir(baseDir + '/img/icons/' + theme): iconsDir = 'icons/' + theme return iconsDir def htmlSearchSharedItems(translate: {}, baseDir: str, searchStr: str, pageNumber: int, resultsPerPage: int, httpPrefix: str, domainFull: str, actor: str, callingDomain: str) -> str: """Search results for shared items """ iconsDir = getIconsDir(baseDir) currPage = 1 ctr = 0 sharedItemsForm = '' searchStrLower = urllib.parse.unquote(searchStr) searchStrLower = searchStrLower.lower().strip('\n').strip('\r') searchStrLowerList = searchStrLower.split('+') cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' with open(cssFilename, 'r') as cssFile: sharedItemsCSS = cssFile.read() if httpPrefix != 'https': sharedItemsCSS = \ sharedItemsCSS.replace('https://', httpPrefix + '://') sharedItemsForm = htmlHeader(cssFilename, sharedItemsCSS) sharedItemsForm += \ '

' + translate['Shared Items Search'] + \ '

' resultsExist = False for subdir, dirs, files in os.walk(baseDir + '/accounts'): for handle in dirs: if '@' not in handle: continue contactNickname = handle.split('@')[0] sharesFilename = baseDir + '/accounts/' + handle + \ '/shares.json' if not os.path.isfile(sharesFilename): continue sharesJson = loadJson(sharesFilename) if not sharesJson: continue for name, sharedItem in sharesJson.items(): matched = True for searchSubstr in searchStrLowerList: subStrMatched = False searchSubstr = searchSubstr.strip() if searchSubstr in sharedItem['location'].lower(): subStrMatched = True elif searchSubstr in sharedItem['summary'].lower(): subStrMatched = True elif searchSubstr in sharedItem['displayName'].lower(): subStrMatched = True elif searchSubstr in sharedItem['category'].lower(): subStrMatched = True if not subStrMatched: matched = False break if matched: if currPage == pageNumber: sharedItemsForm += '
\n' sharedItemsForm += \ '

' + \ sharedItem['displayName'] + '

\n' if sharedItem.get('imageUrl'): sharedItemsForm += \ '\n' sharedItemsForm += \ 'Item image\n' sharedItemsForm += \ '

' + sharedItem['summary'] + '

\n' sharedItemsForm += \ '

' + translate['Type'] + \ ': ' + sharedItem['itemType'] + ' ' sharedItemsForm += \ '' + translate['Category'] + \ ': ' + sharedItem['category'] + ' ' sharedItemsForm += \ '' + translate['Location'] + \ ': ' + sharedItem['location'] + '

\n' contactActor = \ httpPrefix + '://' + domainFull + \ '/users/' + contactNickname sharedItemsForm += \ '

\n' if actor.endswith('/users/' + contactNickname): sharedItemsForm += \ ' \n' sharedItemsForm += '

\n' if not resultsExist and currPage > 1: postActor = \ getAltPath(actor, domainFull, callingDomain) # previous page link, needs to be a POST sharedItemsForm += \ '
\n' sharedItemsForm += \ ' \n' sharedItemsForm += \ '
\n' sharedItemsForm += \ '
\n' + \ ' \n' sharedItemsForm += \ ' ' + translate['Page up'] + \
                                    '\n' sharedItemsForm += '
\n' sharedItemsForm += '
\n' resultsExist = True ctr += 1 if ctr >= resultsPerPage: currPage += 1 if currPage > pageNumber: postActor = \ getAltPath(actor, domainFull, callingDomain) # next page link, needs to be a POST sharedItemsForm += \ '
\n' sharedItemsForm += \ ' \n' sharedItemsForm += \ '
\n' sharedItemsForm += \ '
\n' + \ ' \n' sharedItemsForm += \ ' ' + translate['Page down'] + \
                                    '\n' sharedItemsForm += '
\n' sharedItemsForm += '
\n' break ctr = 0 if not resultsExist: sharedItemsForm += \ '
' + translate['No results'] + '
\n' sharedItemsForm += htmlFooter() return sharedItemsForm def htmlModerationInfo(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' with open(cssFilename, 'r') as cssFile: infoCSS = cssFile.read() 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 htmlHashtagSearch(nickname: str, domain: str, port: int, recentPostsCache: {}, maxRecentPosts: int, translate: {}, baseDir: str, hashtag: str, pageNumber: int, postsPerPage: int, session, wfRequest: {}, personCache: {}, httpPrefix: str, projectVersion: str, YTReplacementDomain: str, showPublishedDateOnly: bool) -> str: """Show a page containing search results for a hashtag """ if hashtag.startswith('#'): hashtag = hashtag[1:] hashtag = urllib.parse.unquote(hashtag) hashtagIndexFile = baseDir + '/tags/' + hashtag + '.txt' if not os.path.isfile(hashtagIndexFile): if hashtag != hashtag.lower(): hashtag = hashtag.lower() hashtagIndexFile = baseDir + '/tags/' + hashtag + '.txt' if not os.path.isfile(hashtagIndexFile): print('WARN: hashtag file not found ' + hashtagIndexFile) return None iconsDir = getIconsDir(baseDir) # check that the directory for the nickname exists if nickname: if not os.path.isdir(baseDir + '/accounts/' + nickname + '@' + domain): nickname = None # read the index with open(hashtagIndexFile, "r") as f: lines = f.readlines() # read the css cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' with open(cssFilename, 'r') as cssFile: hashtagSearchCSS = cssFile.read() if httpPrefix != 'https': hashtagSearchCSS = \ hashtagSearchCSS.replace('https://', httpPrefix + '://') # ensure that the page number is in bounds if not pageNumber: pageNumber = 1 elif pageNumber < 1: pageNumber = 1 # get the start end end within the index file startIndex = int((pageNumber - 1) * postsPerPage) endIndex = startIndex + postsPerPage noOfLines = len(lines) if endIndex >= noOfLines and noOfLines > 0: endIndex = noOfLines - 1 # add the page title hashtagSearchForm = htmlHeader(cssFilename, hashtagSearchCSS) if nickname: hashtagSearchForm += '
\n' + \ '

#' + \ hashtag + '

\n' + '
\n' else: hashtagSearchForm += '
\n' + \ '

#' + hashtag + '

\n' + '
\n' # RSS link for hashtag feed hashtagSearchForm += '
' hashtagSearchForm += \ 'RSS 2.0
' if startIndex > 0: # previous page link hashtagSearchForm += \ '
\n' + \ ' ' + translate['Page up'] + \
            '\n
\n' index = startIndex while index <= endIndex: postId = lines[index].strip('\n').strip('\r') if ' ' not in postId: nickname = getNicknameFromActor(postId) if not nickname: index += 1 continue else: postFields = postId.split(' ') if len(postFields) != 3: index += 1 continue nickname = postFields[1] postId = postFields[2] postFilename = locatePost(baseDir, nickname, domain, postId) if not postFilename: index += 1 continue postJsonObject = loadJson(postFilename) if postJsonObject: if not isPublicPost(postJsonObject): index += 1 continue showIndividualPostIcons = False if nickname: showIndividualPostIcons = True allowDeletion = False hashtagSearchForm += \ individualPostAsHtml(True, recentPostsCache, maxRecentPosts, iconsDir, translate, None, baseDir, session, wfRequest, personCache, nickname, domain, port, postJsonObject, None, True, allowDeletion, httpPrefix, projectVersion, 'search', YTReplacementDomain, showPublishedDateOnly, showIndividualPostIcons, showIndividualPostIcons, False, False, False) index += 1 if endIndex < noOfLines - 1: # next page link hashtagSearchForm += \ '
\n' + \ ' ' + translate['Page down'] + '' + \ '
' hashtagSearchForm += htmlFooter() return hashtagSearchForm def rss2TagHeader(hashtag: str, httpPrefix: str, domainFull: str) -> str: rssStr = "" rssStr += "" rssStr += '' rssStr += ' #' + hashtag + '' rssStr += ' ' + httpPrefix + '://' + domainFull + \ '/tags/rss2/' + hashtag + '' return rssStr def rss2TagFooter() -> str: rssStr = '' rssStr += '' return rssStr def rssHashtagSearch(nickname: str, domain: str, port: int, recentPostsCache: {}, maxRecentPosts: int, translate: {}, baseDir: str, hashtag: str, postsPerPage: int, session, wfRequest: {}, personCache: {}, httpPrefix: str, projectVersion: str, YTReplacementDomain: str) -> str: """Show an rss feed for a hashtag """ if hashtag.startswith('#'): hashtag = hashtag[1:] hashtag = urllib.parse.unquote(hashtag) hashtagIndexFile = baseDir + '/tags/' + hashtag + '.txt' if not os.path.isfile(hashtagIndexFile): if hashtag != hashtag.lower(): hashtag = hashtag.lower() hashtagIndexFile = baseDir + '/tags/' + hashtag + '.txt' if not os.path.isfile(hashtagIndexFile): print('WARN: hashtag file not found ' + hashtagIndexFile) return None # check that the directory for the nickname exists if nickname: if not os.path.isdir(baseDir + '/accounts/' + nickname + '@' + domain): nickname = None # read the index lines = [] with open(hashtagIndexFile, "r") as f: lines = f.readlines() if not lines: return None domainFull = domain if port: if port != 80 and port != 443: domainFull = domain + ':' + str(port) maxFeedLength = 10 hashtagFeed = \ rss2TagHeader(hashtag, httpPrefix, domainFull) for index in range(len(lines)): postId = lines[index].strip('\n').strip('\r') if ' ' not in postId: nickname = getNicknameFromActor(postId) if not nickname: index += 1 if index >= maxFeedLength: break continue else: postFields = postId.split(' ') if len(postFields) != 3: index += 1 if index >= maxFeedLength: break continue nickname = postFields[1] postId = postFields[2] postFilename = locatePost(baseDir, nickname, domain, postId) if not postFilename: index += 1 if index >= maxFeedLength: break continue postJsonObject = loadJson(postFilename) if postJsonObject: if not isPublicPost(postJsonObject): index += 1 if index >= maxFeedLength: break continue # add to feed if postJsonObject['object'].get('content') and \ postJsonObject['object'].get('attributedTo') and \ postJsonObject['object'].get('published'): published = postJsonObject['object']['published'] pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ") rssDateStr = pubDate.strftime("%a, %d %b %Y %H:%M:%S UT") hashtagFeed += ' ' hashtagFeed += \ ' ' + \ postJsonObject['object']['attributedTo'] + \ '' if postJsonObject['object'].get('summary'): hashtagFeed += \ ' ' + \ postJsonObject['object']['summary'] + \ '' hashtagFeed += \ ' ' hashtagFeed += \ ' ' + rssDateStr + '' if postJsonObject['object'].get('attachment'): for attach in postJsonObject['object']['attachment']: if not attach.get('url'): continue hashtagFeed += \ ' ' + attach['url'] + '' hashtagFeed += ' ' index += 1 if index >= maxFeedLength: break return hashtagFeed + rss2TagFooter() def htmlSkillsSearch(translate: {}, baseDir: str, httpPrefix: str, skillsearch: str, instanceOnly: bool, postsPerPage: int) -> str: """Show a page containing search results for a skill """ if skillsearch.startswith('*'): skillsearch = skillsearch[1:].strip() skillsearch = skillsearch.lower().strip('\n').strip('\r') results = [] # search instance accounts for subdir, dirs, files in os.walk(baseDir + '/accounts/'): for f in files: if not f.endswith('.json'): continue if '@' not in f: continue if f.startswith('inbox@'): continue actorFilename = os.path.join(subdir, f) actorJson = loadJson(actorFilename) if actorJson: if actorJson.get('id') and \ actorJson.get('skills') and \ actorJson.get('name') and \ actorJson.get('icon'): actor = actorJson['id'] for skillName, skillLevel in actorJson['skills'].items(): skillName = skillName.lower() if not (skillName in skillsearch or skillsearch in skillName): continue skillLevelStr = str(skillLevel) if skillLevel < 100: skillLevelStr = '0' + skillLevelStr if skillLevel < 10: skillLevelStr = '0' + skillLevelStr indexStr = \ skillLevelStr + ';' + actor + ';' + \ actorJson['name'] + \ ';' + actorJson['icon']['url'] if indexStr not in results: results.append(indexStr) if not instanceOnly: # search actor cache for subdir, dirs, files in os.walk(baseDir + '/cache/actors/'): for f in files: if not f.endswith('.json'): continue if '@' not in f: continue if f.startswith('inbox@'): continue actorFilename = os.path.join(subdir, f) cachedActorJson = loadJson(actorFilename) if cachedActorJson: if cachedActorJson.get('actor'): actorJson = cachedActorJson['actor'] if actorJson.get('id') and \ actorJson.get('skills') and \ actorJson.get('name') and \ actorJson.get('icon'): actor = actorJson['id'] for skillName, skillLevel in \ actorJson['skills'].items(): skillName = skillName.lower() if not (skillName in skillsearch or skillsearch in skillName): continue skillLevelStr = str(skillLevel) if skillLevel < 100: skillLevelStr = '0' + skillLevelStr if skillLevel < 10: skillLevelStr = '0' + skillLevelStr indexStr = \ skillLevelStr + ';' + actor + ';' + \ actorJson['name'] + \ ';' + actorJson['icon']['url'] if indexStr not in results: results.append(indexStr) results.sort(reverse=True) cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' with open(cssFilename, 'r') as cssFile: skillSearchCSS = cssFile.read() if httpPrefix != 'https': skillSearchCSS = \ skillSearchCSS.replace('https://', httpPrefix + '://') skillSearchForm = htmlHeader(cssFilename, skillSearchCSS) skillSearchForm += \ '

' + translate['Skills search'] + ': ' + \ skillsearch + '

' if len(results) == 0: skillSearchForm += \ '
' + translate['No results'] + \ '
' else: skillSearchForm += '
' ctr = 0 for skillMatch in results: skillMatchFields = skillMatch.split(';') if len(skillMatchFields) != 4: continue actor = skillMatchFields[1] actorName = skillMatchFields[2] avatarUrl = skillMatchFields[3] skillSearchForm += \ '
' skillSearchForm += \ '' + actorName + \ '
' ctr += 1 if ctr >= postsPerPage: break skillSearchForm += '
' skillSearchForm += htmlFooter() return skillSearchForm def htmlHistorySearch(translate: {}, baseDir: str, httpPrefix: str, nickname: str, domain: str, historysearch: str, postsPerPage: int, pageNumber: int, projectVersion: str, recentPostsCache: {}, maxRecentPosts: int, session, wfRequest, personCache: {}, port: int, YTReplacementDomain: str, showPublishedDateOnly: bool) -> str: """Show a page containing search results for your post history """ if historysearch.startswith('!'): historysearch = historysearch[1:].strip() historysearch = historysearch.lower().strip('\n').strip('\r') boxFilenames = \ searchBoxPosts(baseDir, nickname, domain, historysearch, postsPerPage) cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' with open(cssFilename, 'r') as cssFile: historySearchCSS = cssFile.read() if httpPrefix != 'https': historySearchCSS = \ historySearchCSS.replace('https://', httpPrefix + '://') historySearchForm = htmlHeader(cssFilename, historySearchCSS) # add the page title historySearchForm += \ '

' + translate['Your Posts'] + '

' if len(boxFilenames) == 0: historySearchForm += \ '
' + translate['No results'] + \ '
' return historySearchForm iconsDir = getIconsDir(baseDir) # ensure that the page number is in bounds if not pageNumber: pageNumber = 1 elif pageNumber < 1: pageNumber = 1 # get the start end end within the index file startIndex = int((pageNumber - 1) * postsPerPage) endIndex = startIndex + postsPerPage noOfBoxFilenames = len(boxFilenames) if endIndex >= noOfBoxFilenames and noOfBoxFilenames > 0: endIndex = noOfBoxFilenames - 1 index = startIndex while index <= endIndex: postFilename = boxFilenames[index] if not postFilename: index += 1 continue postJsonObject = loadJson(postFilename) if not postJsonObject: index += 1 continue showIndividualPostIcons = True allowDeletion = False historySearchForm += \ individualPostAsHtml(True, recentPostsCache, maxRecentPosts, iconsDir, translate, None, baseDir, session, wfRequest, personCache, nickname, domain, port, postJsonObject, None, True, allowDeletion, httpPrefix, projectVersion, 'search', YTReplacementDomain, showPublishedDateOnly, showIndividualPostIcons, showIndividualPostIcons, False, False, False) index += 1 historySearchForm += htmlFooter() return historySearchForm def scheduledPostsExist(baseDir: str, nickname: str, domain: str) -> bool: """Returns true if there are posts scheduled to be delivered """ scheduleIndexFilename = \ baseDir + '/accounts/' + nickname + '@' + domain + '/schedule.index' if not os.path.isfile(scheduleIndexFilename): return False if '#users#' in open(scheduleIndexFilename).read(): return True return False def htmlEditLinks(translate: {}, baseDir: str, path: str, domain: str, port: int, httpPrefix: str) -> str: """Shows the edit links screen """ if '/users/' not in path: return '' pathOriginal = path 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' with open(cssFilename, 'r') as cssFile: editCSS = cssFile.read() if httpPrefix != 'https': editCSS = \ editCSS.replace('https://', httpPrefix + '://') editLinksForm = htmlHeader(cssFilename, editCSS) editLinksForm += \ '
\n' editLinksForm += \ '
\n' editLinksForm += \ '

' + translate['Edit Links'] + '

' editLinksForm += \ '
\n' editLinksForm += \ ' \n' editLinksForm += \ ' \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(translate: {}, baseDir: str, path: str, domain: str, port: int, httpPrefix: str) -> str: """Shows the edit newswire screen """ if '/users/' not in path: return '' pathOriginal = path 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' with open(cssFilename, 'r') as cssFile: editCSS = cssFile.read() if httpPrefix != 'https': editCSS = \ editCSS.replace('https://', httpPrefix + '://') editNewswireForm = htmlHeader(cssFilename, editCSS) editNewswireForm += \ '\n' editNewswireForm += \ '
\n' editNewswireForm += \ '

' + translate['Edit newswire'] + '

' editNewswireForm += \ '
\n' editNewswireForm += \ ' \n' editNewswireForm += \ ' \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 += \ ' ' editNewswireForm += \ '
' editNewswireForm += htmlFooter() return editNewswireForm def htmlEditNewsPost(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' with open(cssFilename, 'r') as cssFile: editCSS = cssFile.read() 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(translate: {}, baseDir: str, path: str, domain: str, port: int, httpPrefix: str) -> str: """Shows the edit profile screen """ imageFormats = '.png, .jpg, .jpeg, .gif, .webp, .avif' pathOriginal = path path = path.replace('/inbox', '').replace('/outbox', '') path = path.replace('/shares', '') nickname = getNicknameFromActor(path) if not nickname: return '' domainFull = domain if port: if port != 80 and port != 443: if ':' not in domain: domainFull = domain + ':' + str(port) actorFilename = \ baseDir + '/accounts/' + nickname + '@' + domain + '.json' if not os.path.isfile(actorFilename): return '' isBot = '' isGroup = '' followDMs = '' removeTwitter = '' notifyLikes = '' hideLikeButton = '' mediaInstanceStr = '' blogsInstanceStr = '' newsInstanceStr = '' displayNickname = nickname bioStr = '' donateUrl = '' emailAddress = '' PGPpubKey = '' PGPfingerprint = '' xmppAddress = '' matrixAddress = '' ssbAddress = '' blogAddress = '' toxAddress = '' manuallyApprovesFollowers = '' actorJson = loadJson(actorFilename) if actorJson: donateUrl = getDonationUrl(actorJson) xmppAddress = getXmppAddress(actorJson) matrixAddress = getMatrixAddress(actorJson) ssbAddress = getSSBAddress(actorJson) blogAddress = getBlogAddress(actorJson) toxAddress = getToxAddress(actorJson) emailAddress = getEmailAddress(actorJson) PGPpubKey = getPGPpubKey(actorJson) PGPfingerprint = getPGPfingerprint(actorJson) if actorJson.get('name'): displayNickname = actorJson['name'] if actorJson.get('summary'): bioStr = \ actorJson['summary'].replace('

', '').replace('

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

' skillsStr += \ '

' skillCtr += 1 skillsStr += \ '

' skillsStr += \ '

' cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' with open(cssFilename, 'r') as cssFile: editProfileCSS = cssFile.read() if httpPrefix != 'https': editProfileCSS = \ editProfileCSS.replace('https://', httpPrefix + '://') instanceStr = '' moderatorsStr = '' themesDropdown = '' adminNickname = getConfigParam(baseDir, 'admin') if 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(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' with open(cssFilename, 'r') as cssFile: loginCSS = cssFile.read() # show the register button registerButtonStr = '' if getConfigParam(baseDir, 'registration') == 'open': if int(getConfigParam(baseDir, 'registrationsRemaining')) > 0: if accounts > 0: idx = 'Welcome. Please login or register a new account.' loginText = \ '' 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(baseDir: str, httpPrefix: str, domainFull: str) -> str: """Show the terms of service screen """ adminNickname = getConfigParam(baseDir, 'admin') if not os.path.isfile(baseDir + '/accounts/tos.txt'): copyfile(baseDir + '/default_tos.txt', baseDir + '/accounts/tos.txt') if os.path.isfile(baseDir + '/accounts/login-background-custom.jpg'): if not os.path.isfile(baseDir + '/accounts/login-background.jpg'): copyfile(baseDir + '/accounts/login-background-custom.jpg', baseDir + '/accounts/login-background.jpg') TOSText = 'Terms of Service go here.' if os.path.isfile(baseDir + '/accounts/tos.txt'): with open(baseDir + '/accounts/tos.txt', 'r') as file: TOSText = file.read() TOSForm = '' cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' with open(cssFilename, 'r') as cssFile: termsCSS = cssFile.read() if httpPrefix != 'https': termsCSS = termsCSS.replace('https://', httpPrefix+'://') TOSForm = htmlHeader(cssFilename, termsCSS) TOSForm += '
' + TOSText + '
\n' if adminNickname: adminActor = httpPrefix + '://' + domainFull + \ '/users/' + adminNickname TOSForm += \ '
\n' + \ '

Administered by ' + adminNickname + '

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

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

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

Administered by ' + adminNickname + '

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

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

\n' blockedHashtagForm += \ '

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

\n' blockedHashtagForm += '
\n' blockedHashtagForm += htmlFooter() return blockedHashtagForm def htmlSuspended(baseDir: str) -> str: """Show the screen for suspended accounts """ suspendedForm = '' cssFilename = baseDir + '/epicyon-suspended.css' if os.path.isfile(baseDir + '/suspended.css'): cssFilename = baseDir + '/suspended.css' with open(cssFilename, 'r') as cssFile: suspendedCSS = cssFile.read() suspendedForm = htmlHeader(cssFilename, suspendedCSS) suspendedForm += '
\n' suspendedForm += '

Account Suspended

\n' suspendedForm += '

See Terms of Service

\n' suspendedForm += '
\n' suspendedForm += htmlFooter() return suspendedForm def htmlNewPost(mediaInstance: bool, translate: {}, baseDir: str, httpPrefix: str, path: str, inReplyTo: str, mentions: [], reportUrl: str, pageNumber: int, nickname: str, domain: str, domainFull: str) -> str: """New post screen """ iconsDir = getIconsDir(baseDir) replyStr = '' showPublicOnDropdown = True messageBoxHeight = 400 if not path.endswith('/newshare'): if not path.endswith('/newreport'): if not inReplyTo or path.endswith('/newreminder'): newPostText = '

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

\n' else: newPostText = \ '

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

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

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

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

' not in customReportText: customReportText = \ '\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' with open(cssFilename, 'r') as cssFile: newPostCSS = cssFile.read() if httpPrefix != 'https': newPostCSS = newPostCSS.replace('https://', httpPrefix + '://') if '?' in path: path = path.split('?')[0] pathBase = path.replace('/newreport', '').replace('/newpost', '') pathBase = pathBase.replace('/newblog', '').replace('/newshare', '') pathBase = pathBase.replace('/newunlisted', '') pathBase = pathBase.replace('/newevent', '') pathBase = pathBase.replace('/newreminder', '') pathBase = pathBase.replace('/newfollowers', '').replace('/newdm', '') newPostImageSection = '
' if not path.endswith('/newevent'): newPostImageSection += \ ' \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' scopeDescription = translate['Blog'] endpoint = 'newblog' elif path.endswith('/newunlisted'): scopeIcon = 'scope_unlisted.png' scopeDescription = translate['Unlisted'] endpoint = 'newunlisted' elif path.endswith('/newfollowers'): scopeIcon = 'scope_followers.png' scopeDescription = translate['Followers'] endpoint = 'newfollowers' elif path.endswith('/newdm'): scopeIcon = 'scope_dm.png' scopeDescription = translate['DM'] endpoint = 'newdm' elif path.endswith('/newreminder'): scopeIcon = 'scope_reminder.png' scopeDescription = translate['Reminder'] endpoint = 'newreminder' elif path.endswith('/newevent'): scopeIcon = 'scope_event.png' scopeDescription = translate['Event'] endpoint = 'newevent' placeholderSubject = translate['Event name'] placeholderMessage = translate['Describe the event'] + '...' elif path.endswith('/newreport'): scopeIcon = 'scope_report.png' scopeDescription = translate['Report'] endpoint = 'newreport' elif path.endswith('/newquestion'): scopeIcon = 'scope_question.png' scopeDescription = translate['Question'] placeholderMessage = translate['Enter your question'] + '...' endpoint = 'newquestion' extraFields = '
\n' extraFields += '
\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' 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) # only show the share option if this is not a reply shareOptionOnDropdown = '' questionOptionOnDropdown = '' if not replyStr: shareOptionOnDropdown = \ '
  • ' + translate['Shares'] + \ '
    ' + translate['Describe a shared item'] + '
  • \n' questionOptionOnDropdown = \ '
  • ' + translate['Question'] + \ '
    ' + translate['Ask a question'] + '
  • \n' mentionsStr = '' for m in mentions: mentionNickname = getNicknameFromActor(m) if not mentionNickname: continue mentionDomain, mentionPort = getDomainFromActor(m) if not mentionDomain: continue if mentionPort: mentionsHandle = \ '@' + mentionNickname + '@' + \ mentionDomain + ':' + str(mentionPort) else: mentionsHandle = '@' + mentionNickname + '@' + mentionDomain if mentionsHandle not in mentionsStr: mentionsStr += mentionsHandle + ' ' # build suffixes so that any replies or mentions are # preserved when switching between scopes dropdownNewPostSuffix = '/newpost' dropdownNewBlogSuffix = '/newblog' dropdownUnlistedSuffix = '/newunlisted' dropdownFollowersSuffix = '/newfollowers' dropdownDMSuffix = '/newdm' dropdownEventSuffix = '/newevent' dropdownReminderSuffix = '/newreminder' dropdownReportSuffix = '/newreport' if inReplyTo or mentions: dropdownNewPostSuffix = '' dropdownNewBlogSuffix = '' dropdownUnlistedSuffix = '' dropdownFollowersSuffix = '' dropdownDMSuffix = '' dropdownEventSuffix = '' dropdownReminderSuffix = '' dropdownReportSuffix = '' if inReplyTo: dropdownNewPostSuffix += '?replyto=' + inReplyTo dropdownNewBlogSuffix += '?replyto=' + inReplyTo dropdownUnlistedSuffix += '?replyto=' + inReplyTo dropdownFollowersSuffix += '?replyfollowers=' + inReplyTo dropdownDMSuffix += '?replydm=' + inReplyTo for mentionedActor in mentions: dropdownNewPostSuffix += '?mention=' + mentionedActor dropdownNewBlogSuffix += '?mention=' + mentionedActor dropdownUnlistedSuffix += '?mention=' + mentionedActor dropdownFollowersSuffix += '?mention=' + mentionedActor dropdownDMSuffix += '?mention=' + mentionedActor dropdownReportSuffix += '?mention=' + mentionedActor dropDownContent = '' if not reportUrl: dropDownContent += "\n' dropDownContent += ' \n' dropDownContent += ' \n' dropDownContent += '
    \n' 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' 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+dateAndLocation if not mediaInstance or replyStr: newPostForm += newPostImageSection newPostForm += '
    \n' newPostForm += '
    \n' if not reportUrl: newPostForm = \ newPostForm.replace('', '') newPostForm += htmlFooter() return newPostForm def getFontFromCss(css: str) -> (str, str): """Returns the font name and format """ if ' url(' not in css: return None, None fontName = css.split(" url(")[1].split(")")[0].replace("'", '') fontFormat = css.split(" format('")[1].split("')")[0] return fontName, fontFormat def htmlHeader(cssFilename: str, css: str, lang='en') -> str: htmlStr = '\n' htmlStr += '\n' htmlStr += ' \n' htmlStr += ' \n' fontName, fontFormat = getFontFromCss(css) if fontName: htmlStr += ' \n' htmlStr += ' \n' htmlStr += ' \n' htmlStr += ' \n' htmlStr += ' Epicyon\n' htmlStr += ' \n' htmlStr += ' \n' return htmlStr def htmlFooter() -> str: htmlStr = ' \n' htmlStr += '\n' return htmlStr def htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int, translate: {}, baseDir: str, httpPrefix: str, authorized: bool, nickname: str, domain: str, port: int, session, wfRequest: {}, personCache: {}, projectVersion: str, YTReplacementDomain: str, showPublishedDateOnly: bool) -> str: """Shows posts on the profile screen These should only be public posts """ iconsDir = getIconsDir(baseDir) profileStr = '' maxItems = 4 ctr = 0 currPage = 1 while ctr < maxItems and currPage < 4: outboxFeed = \ personBoxJson({}, session, baseDir, domain, port, '/users/' + nickname + '/outbox?page=' + str(currPage), httpPrefix, 10, 'outbox', authorized, 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 += 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 sharesTimelineJson(actor: str, pageNumber: int, itemsPerPage: int, baseDir: str, maxSharesPerAccount: int) -> ({}, bool): """Get a page on the shared items timeline as json maxSharesPerAccount helps to avoid one person dominating the timeline by sharing a large number of things """ allSharesJson = {} for subdir, dirs, files in os.walk(baseDir + '/accounts'): for handle in dirs: if '@' in handle: accountDir = baseDir + '/accounts/' + handle sharesFilename = accountDir + '/shares.json' if os.path.isfile(sharesFilename): sharesJson = loadJson(sharesFilename) if not sharesJson: continue nickname = handle.split('@')[0] # actor who owns this share owner = actor.split('/users/')[0] + '/users/' + nickname ctr = 0 for itemID, item in sharesJson.items(): # assign owner to the item item['actor'] = owner allSharesJson[str(item['published'])] = item ctr += 1 if ctr >= maxSharesPerAccount: break # sort the shared items in descending order of publication date sharesJson = OrderedDict(sorted(allSharesJson.items(), reverse=True)) lastPage = False startIndex = itemsPerPage*pageNumber maxIndex = len(sharesJson.items()) if maxIndex < itemsPerPage: lastPage = True if startIndex >= maxIndex - itemsPerPage: lastPage = True startIndex = maxIndex - itemsPerPage if startIndex < 0: startIndex = 0 ctr = 0 resultJson = {} for published, item in sharesJson.items(): if ctr >= startIndex + itemsPerPage: break if ctr < startIndex: ctr += 1 continue resultJson[published] = item ctr += 1 return resultJson, lastPage def htmlSharesTimeline(translate: {}, pageNumber: int, itemsPerPage: int, baseDir: str, actor: str, nickname: str, domain: str, port: int, maxSharesPerAccount: int, httpPrefix: str) -> str: """Show shared items timeline as html """ sharesJson, lastPage = \ sharesTimelineJson(actor, pageNumber, itemsPerPage, baseDir, maxSharesPerAccount) domainFull = domain if port != 80 and port != 443: if ':' not in domain: domainFull = domain + ':' + str(port) actor = httpPrefix + '://' + domainFull + '/users/' + nickname timelineStr = '' if pageNumber > 1: iconsDir = getIconsDir(baseDir) timelineStr += \ '
    \n' + \ ' ' + translate['Page up'] + '\n' + \ '
    \n' for published, item in sharesJson.items(): showContactButton = False if item['actor'] != actor: showContactButton = True showRemoveButton = False if item['actor'] == actor: showRemoveButton = True timelineStr += \ htmlIndividualShare(actor, item, translate, showContactButton, showRemoveButton) if not lastPage: iconsDir = getIconsDir(baseDir) timelineStr += \ '
    \n' + \ ' ' + translate['Page down'] + '\n' + \ '
    \n' return timelineStr def htmlProfile(defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, projectVersion: str, baseDir: str, httpPrefix: str, authorized: bool, profileJson: {}, selected: str, session, wfRequest: {}, personCache: {}, YTReplacementDomain: str, showPublishedDateOnly: bool, extraJson=None, pageNumber=None, maxItemsPerPage=None) -> str: """Show the profile page as html """ nickname = profileJson['preferredUsername'] if not nickname: return "" domain, port = getDomainFromActor(profileJson['id']) if not domain: return "" displayName = \ addEmojiToDisplayName(baseDir, httpPrefix, nickname, domain, profileJson['name'], True) domainFull = domain if port: domainFull = domain + ':' + str(port) profileDescription = \ addEmojiToDisplayName(baseDir, httpPrefix, nickname, domain, profileJson['summary'], False) postsButton = 'button' followingButton = 'button' followersButton = 'button' rolesButton = 'button' skillsButton = 'button' sharesButton = 'button' if selected == 'posts': postsButton = 'buttonselected' elif selected == 'following': followingButton = 'buttonselected' elif selected == 'followers': followersButton = 'buttonselected' elif selected == 'roles': rolesButton = 'buttonselected' elif selected == 'skills': skillsButton = 'buttonselected' elif selected == 'shares': sharesButton = 'buttonselected' loginButton = '' followApprovalsSection = '' followApprovals = False linkToTimelineStart = '' linkToTimelineEnd = '' editProfileStr = '' logoutStr = '' actor = profileJson['id'] usersPath = '/users/' + actor.split('/users/')[1] donateSection = '' donateUrl = getDonationUrl(profileJson) PGPpubKey = getPGPpubKey(profileJson) PGPfingerprint = getPGPfingerprint(profileJson) emailAddress = getEmailAddress(profileJson) xmppAddress = getXmppAddress(profileJson) matrixAddress = getMatrixAddress(profileJson) ssbAddress = getSSBAddress(profileJson) toxAddress = getToxAddress(profileJson) if donateUrl or xmppAddress or matrixAddress or \ ssbAddress or toxAddress or PGPpubKey or \ PGPfingerprint or emailAddress: donateSection = '
    \n' donateSection += '
    \n' if donateUrl: donateSection += \ '

    \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' if not authorized: loginButton = \ '
    ' else: editProfileStr = \ '' logoutStr = \ '' 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('

    ', '') profileHeaderStr = '
    ' profileHeaderStr += '
    ' profileHeaderStr += \ ' ' + \
        avatarDescription + '' profileHeaderStr += '

    ' + displayName + '

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

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

    ' profileHeaderStr += '

    ' + profileDescriptionShort + '

    ' profileHeaderStr += loginButton profileHeaderStr += '
    ' profileHeaderStr += '
    ' profileStr = \ linkToTimelineStart + profileHeaderStr + \ linkToTimelineEnd + donateSection profileStr += '
    \n' profileStr += '
    ' profileStr += \ ' ' profileStr += \ ' ' + \ '' profileStr += \ ' ' + \ '' profileStr += \ ' ' + \ '' profileStr += \ ' ' + \ '' profileStr += \ ' ' + \ '' profileStr += editProfileStr + logoutStr profileStr += '
    ' profileStr += '
    ' profileStr += followApprovalsSection cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): cssFilename = baseDir + '/epicyon.css' with open(cssFilename, 'r') as cssFile: profileStyle = \ cssFile.read().replace('image.png', profileJson['image']['url']) licenseStr = \ '' + \ '' + \
            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 profileStr = \ htmlHeader(cssFilename, profileStyle) + profileStr + htmlFooter() return profileStr def individualFollowAsHtml(translate: {}, baseDir: str, session, wfRequest: {}, personCache: {}, domain: str, followUrl: str, authorized: bool, actorNickname: str, httpPrefix: str, projectVersion: str, buttons=[]) -> str: """An individual follow entry on the profile screen """ nickname = getNicknameFromActor(followUrl) domain, port = getDomainFromActor(followUrl) titleStr = '@' + nickname + '@' + domain avatarUrl = getPersonAvatarUrl(baseDir, followUrl, personCache, True) if not avatarUrl: avatarUrl = followUrl + '/avatar.png' if domain not in followUrl: (inboxUrl, pubKeyId, pubKey, fromPersonId, sharedInbox, avatarUrl2, displayName) = getPersonBox(baseDir, session, wfRequest, personCache, projectVersion, httpPrefix, nickname, domain, 'outbox') if avatarUrl2: avatarUrl = avatarUrl2 if displayName: titleStr = displayName + ' ' + titleStr buttonsStr = '' if authorized: for b in buttons: if b == 'block': buttonsStr += \ '\n' if b == 'unfollow': buttonsStr += \ '\n' resultStr = '
    \n' resultStr += \ '\n' resultStr += '

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

    \n' resultStr += '
    \n' return resultStr def addEmbeddedAudio(translate: {}, content: str) -> str: """Adds embedded audio for mp3/ogg """ if not ('.mp3' in content or '.ogg' in content): return content if '