__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 xmpp import getXmppAddress
from ssb import getSSBAddress
from tox import getToxAddress
from matrix import getMatrixAddress
from donate import getDonationUrl
from utils import getFileCaseInsensitive
from utils import searchBoxPosts
from utils import isBlogPost
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 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 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 switchWords
from content import getMentionsFromHtml
from content import addHtmlTags
from content import replaceEmojiFromTags
from content import removeLongWords
from config import getConfigParam
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
def getContentWarningButton(postID: str, translate: {},
                            content: str) -> str:
    """Returns the markup for a content warning button
    """
    return '' + translate['SHOW MORE'] + ' ' + \
        '
'
                            sharedItemsForm += \
                                '
' + \
                                sharedItem['displayName'] + '
'
                            if sharedItem.get('imageUrl'):
                                sharedItemsForm += \
                                    '
'
                                sharedItemsForm += \
                                    ' '
                            sharedItemsForm += \
                                '
' + sharedItem['summary'] + '
'
                            sharedItemsForm += \
                                '
' + translate['Type'] + \
                                ':  ' + sharedItem['itemType'] + ' '
                            sharedItemsForm += \
                                '' + translate['Category'] + \
                                ':  ' + sharedItem['category'] + ' '
                            sharedItemsForm += \
                                '' + translate['Location'] + \
                                ':  ' + sharedItem['location'] + '
'
                            contactActor = \
                                httpPrefix + '://' + domainFull + \
                                '/users/' + contactNickname
                            sharedItemsForm += \
                                '
' + \
                                translate['Contact'] + ' ' + \
                                    translate['Remove'] + ' 
')
    editProfileForm = htmlHeader(cssFilename, editProfileCSS)
    editProfileForm += \
        ''
    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
    if not loginImageFilename:
        loginImageFilename = baseDir + '/accounts/' + loginImage
        copyfile(baseDir + '/img/login.png', loginImageFilename)
    if os.path.isfile(baseDir + '/img/login-background.png'):
        if not os.path.isfile(baseDir + '/accounts/login-background.png'):
            copyfile(baseDir + '/img/login-background.png',
                     baseDir + '/accounts/login-background.png')
    if accounts > 0:
        loginText = \
            '' + \
            translate['Welcome. Please enter your login details below.'] + \
            '
'
    else:
        loginText = \
            '' + \
            translate['Please enter some credentials'] + '
'
        loginText += \
            '' + \
            translate['You will become the admin of this site.'] + \
            '
'
    if os.path.isfile(baseDir + '/accounts/login.txt'):
        # custom login message
        with open(baseDir + '/accounts/login.txt', 'r') as file:
            loginText = '' + file.read() + '
'
    cssFilename = baseDir + '/epicyon-login.css'
    if os.path.isfile(baseDir + '/login.css'):
        cssFilename = baseDir + '/login.css'
    with open(cssFilename, 'r') as cssFile:
        loginCSS = cssFile.read()
    # show the register button
    registerButtonStr = ''
    if getConfigParam(baseDir, 'registration') == 'open':
        if int(getConfigParam(baseDir, 'registrationsRemaining')) > 0:
            if accounts > 0:
                idx = 'Welcome. Please login or register a new account.'
                loginText = \
                    '' + \
                    translate[idx] + \
                    '
'
            registerButtonStr = \
                'Register '
    TOSstr = \
        '' + \
        translate['Terms of Service'] + ' 
'
    TOSstr += \
        '' + \
        translate['About this Instance'] + ' 
'
    loginButtonStr = ''
    if accounts > 0:
        loginButtonStr = \
            '' + \
            translate['Login'] + ' '
    autocompleteStr = ''
    if not autocomplete:
        autocompleteStr = 'autocomplete="off" value=""'
    loginForm = htmlHeader(cssFilename, loginCSS)
    loginForm += ''
    loginForm += '  '
    loginForm += \
        '    
'
    loginForm += loginText + TOSstr
    loginForm += '  
'
    loginForm += '    ' + \
        translate['Nickname'] + ' ' + \
        translate['Password'] + ' 
'
    loginForm += ' '
    loginForm += \
        '' + \
        ' '
    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 + '/img/login-background.png'):
        if not os.path.isfile(baseDir + '/accounts/login-background.png'):
            copyfile(baseDir + '/img/login-background.png',
                     baseDir + '/accounts/login-background.png')
    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 + '
'
        if adminNickname:
            adminActor = httpPrefix + '://' + domainFull + \
                '/users/' + adminNickname
            TOSForm += \
                ''
        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 + '/img/login-background.png'):
        if not os.path.isfile(baseDir + '/accounts/login-background.png'):
            copyfile(baseDir + '/img/login-background.png',
                     baseDir + '/accounts/login-background.png')
    aboutText = 'Information about this instance goes here.'
    if os.path.isfile(baseDir + '/accounts/about.txt'):
        with open(baseDir + '/accounts/about.txt', 'r') as file:
            aboutText = file.read()
    aboutForm = ''
    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 != 'http':
            termsCSS = termsCSS.replace('https://',
                                        httpPrefix + '://')
        aboutForm = htmlHeader(cssFilename, termsCSS)
        aboutForm += '' + aboutText + '
'
        if onionDomain:
            aboutForm += \
                '' + \
                'http://' + onionDomain + '
'
        blockedHashtagForm += '  Hashtag Blocked
'
        blockedHashtagForm += \
            '  See Terms of Service 
'
        blockedHashtagForm += ' '
        suspendedForm += '  Account Suspended
'
        suspendedForm += '  See Terms of Service 
'
        suspendedForm += ' ' + \
                    translate['Write your post text below.'] + '
'
            else:
                newPostText = \
                    '' + \
                    translate['Write your reply to'] + \
                    ' ' + \
                    translate['this post'] + ' 
'
                replyStr = '' + \
                translate['Write your report below.'] + '
'
            # custom report header with any additional instructions
            if os.path.isfile(baseDir + '/accounts/report.txt'):
                with open(baseDir + '/accounts/report.txt', 'r') as file:
                    customReportText = file.read()
                    if '' not in customReportText:
                        customReportText = \
                            '' + \
                            customReportText + '
'
                        repStr = ''
                        customReportText = \
                            customReportText.replace('
', repStr)
                        newPostText += customReportText
            idx = 'This message only goes to moderators, even if it ' + \
                'mentions other fediverse addresses.'
            newPostText += \
                '
' + translate[idx] + \
                '
' + translate['Also see'] + \
                ' ' + \
                translate['Terms of Service'] + ' 
'
    else:
        newPostText = \
            '' + \
            translate['Enter the details for your shared item below.'] + '
'
    if path.endswith('/newquestion'):
        newPostText = \
            '' + \
            translate['Enter the choices for your question below.'] + '
'
    if os.path.isfile(baseDir + '/accounts/newpost.txt'):
        with open(baseDir + '/accounts/newpost.txt', 'r') as file:
            newPostText = \
                '' + file.read() + '
'
    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('/newfollowers', '').replace('/newdm', '')
    newPostImageSection = '    '
    newPostImageSection += \
        '      ' + translate['Image description'] + \
        ' '
    newPostImageSection += '      
'
    scopeIcon = 'scope_public.png'
    scopeDescription = translate['Public']
    placeholderSubject = \
        translate['Subject or Content Warning (optional)'] + '...'
    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('/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 = ''
        extraFields += '  ' + \
            translate['Possible answers'] + ': ' + \
            translate['Duration of listing in days'] + \
            ':  
'
    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 = ''
        extraFields += \
            '  ' + \
            translate['Type of shared item. eg. hat'] + ': '
        extraFields += '  ' + \
            translate['Category of shared item. eg. clothing'] + ': '
        extraFields += '  ' + \
            translate['Duration of listing in days'] + ': '
        extraFields += '  
'
        extraFields += ''
        extraFields += \
            '' + \
            translate['City or location of the shared item'] + ': '
        extraFields += '
'
    dateAndLocation = ''
    if endpoint != 'newshare' and \
       endpoint != 'newreport' and \
       endpoint != 'newquestion':
        dateAndLocation = ''
        dateAndLocation += ''
        dateAndLocation += '' + \
            translate['Location'] + ':  '
        dateAndLocation += '
'
    newPostForm = htmlHeader(cssFilename, newPostCSS)
    # only show the share option if this is not a reply
    shareOptionOnDropdown = ''
    questionOptionOnDropdown = ''
    if not replyStr:
        shareOptionOnDropdown = \
            '' + translate['Shares'] + \
            ' ' + translate['Question'] + \
            ' '
    newPostForm += '  '
    newPostForm += \
        '    
' + newPostText + ' '
    newPostForm += '    
'
    newPostForm += '      
'
    newPostForm += \
        '        
' + \
        scopeDescription + ' '
    newPostForm += dropDownContent
    newPostForm += '      
'
    newPostForm += \
        '      
'
    newPostForm += '    
'
    newPostForm += '    
'
    newPostForm += replyStr
    if mediaInstance and not replyStr:
        newPostForm += newPostImageSection
    newPostForm += \
        '    
' + placeholderSubject + ' '
    newPostForm += '    
'
    newPostForm += ''
    newPostForm += \
        '    
' + placeholderMessage + ' '
    messageBoxHeight = 400
    if mediaInstance:
        messageBoxHeight = 200
    if endpoint == 'newquestion':
        messageBoxHeight = 100
    elif endpoint == 'newblog':
        messageBoxHeight = 800
    newPostForm += \
        '    
' + mentionsStr + ' '
    newPostForm += extraFields+dateAndLocation
    if not mediaInstance or replyStr:
        newPostForm += newPostImageSection
    newPostForm += '  
 '
    if not reportUrl:
        newPostForm += \
            ''
        newPostForm = \
            newPostForm.replace('', '')
    newPostForm += htmlFooter()
    return newPostForm
def htmlHeader(cssFilename: str, css=None, refreshSec=0, lang='en') -> str:
    if refreshSec == 0:
        meta = '  ' + project + \
            ' '
        for role in rolesList:
            profileStr += '
' + role + ' '
        profileStr += '@' + nickname + '@' + domain + ' has no roles assigned
'
    else:
        profileStr = '' + profileStr + '
'
    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 += \
            '' + \
            profileStr + '
'
    profileStr += '
' + item['displayName'] + '
'
    if item.get('imageUrl'):
        profileStr += '
'
        profileStr += \
            ' '
    profileStr += '
' + item['summary'] + '
'
    profileStr += \
        '
' + translate['Type'] + ':  ' + item['itemType'] + ' '
    profileStr += \
        '' + translate['Category'] + ':  ' + item['category'] + ' '
    profileStr += \
        '' + translate['Location'] + ':  ' + item['location'] + '
'
    if showContact:
        contactActor = item['actor']
        profileStr += \
            '
' + \
            translate['Contact'] + ' ' + \
            translate['Remove'] + ' 
' + profileStr + '
'
    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'
        donateSection += '  
\n'
        if donateUrl:
            donateSection += \
                '    ' + translate['Donate'] + \
                ' 
\n'
        if emailAddress:
            donateSection += \
                '' + translate['Email'] + ': ' + emailAddress + ' 
\n'
        if xmppAddress:
            donateSection += \
                '' + translate['XMPP'] + ': '+xmppAddress + ' 
\n'
        if matrixAddress:
            donateSection += \
                '' + translate['Matrix'] + ': ' + matrixAddress + '
\n'
        if ssbAddress:
            donateSection += \
                'SSB: ' + \
                ssbAddress + ' 
\n'
        if toxAddress:
            donateSection += \
                'Tox: ' + \
                toxAddress + ' 
\n'
        if PGPpubKey:
            donateSection += \
                '' + PGPpubKey.replace('\n', '
\n'
        donateSection += '   \n'
        donateSection += '
' + \
            translate['Login'] + ' ' + \
            translate['Edit'] + '  ' + \
            translate['Logout'] + '  ' + \
            translate['Switch to timeline view'] + ' '
        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 = httpPrefix + '://' + domainFull + \
                                '/users/' + nickname
                            followApprovalsSection += ''
    profileDescriptionShort = profileDescription
    if '\n' in profileDescription:
        if len(profileDescription.split('\n')) > 2:
            profileDescriptionShort = ''
    else:
        if '', '')
        avatarDescription = avatarDescription.replace('
', '')
    profileHeaderStr = ''
    profileHeaderStr += '  
'
    profileHeaderStr += \
        '    
'
    profileHeaderStr += '    
' + displayName + ' '
    profileHeaderStr += \
        '    
@' + nickname + '@' + domainFull + ' 
'
    profileHeaderStr += '    
' + profileDescriptionShort + '
'
    profileHeaderStr += loginButton
    profileHeaderStr += '  
'
    profileHeaderStr += '
' + \
            ' '
        if selected == 'posts':
            profileStr += \
                htmlProfilePosts(recentPostsCache, maxRecentPosts,
                                 translate,
                                 baseDir, httpPrefix, authorized,
                                 ocapAlways, nickname, domain, port,
                                 session, wfRequest, personCache,
                                 projectVersion) + licenseStr
        if selected == 'following':
            profileStr += \
                htmlProfileFollowing(translate, baseDir, httpPrefix,
                                     authorized, ocapAlways, nickname,
                                     domain, port, session,
                                     wfRequest, personCache, extraJson,
                                     projectVersion, ["unfollow"], selected,
                                     actor, pageNumber, maxItemsPerPage)
        if selected == 'followers':
            profileStr += \
                htmlProfileFollowing(translate, baseDir, httpPrefix,
                                     authorized, ocapAlways, nickname,
                                     domain, port, session,
                                     wfRequest, personCache, extraJson,
                                     projectVersion, ["block"],
                                     selected, actor, 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)
    if not avatarUrl:
        avatarUrl = followUrl + '/avatar.png'
    if domain not in followUrl:
        (inboxUrl, pubKeyId, pubKey,
         fromPersonId, sharedInbox,
         capabilityAcquisition,
         avatarUrl2, displayName) = getPersonBox(baseDir, session, wfRequest,
                                                 personCache, projectVersion,
                                                 httpPrefix, nickname,
                                                 domain, 'outbox')
        if avatarUrl2:
            avatarUrl = avatarUrl2
        if displayName:
            titleStr = displayName + ' ' + titleStr
    buttonsStr = ''
    if authorized:
        for b in buttons:
            if b == 'block':
                buttonsStr += \
                    '' + \
                    translate['Block'] + ' ' + \
                    translate['Unfollow'] + ' ', '')
        if w.endswith('.'):
            w = w[:-1]
        if w.endswith('"'):
            w = w[:-1]
        if w.endswith(';'):
            w = w[:-1]
        if w.endswith(':'):
            w = w[:-1]
        if not w.endswith(extension):
            continue
        if not (w.startswith('http') or w.startswith('dat:') or
                w.startswith('hyper:') or w.startswith('i2p:') or
                '/' in w):
            continue
        url = w
        content += ''
        content += \
            ''
        content += \
            translate['Your browser does not support the audio element.']
        content += '  ', '')
        if w.endswith('.'):
            w = w[:-1]
        if w.endswith('"'):
            w = w[:-1]
        if w.endswith(';'):
            w = w[:-1]
        if w.endswith(':'):
            w = w[:-1]
        if not w.endswith(extension):
            continue
        if not (w.startswith('http') or w.startswith('dat:') or
                w.startswith('hyper:') or w.startswith('i2p:') or
                '/' in w):
            continue
        url = w
        content += \
            ''
        content += \
            ''
        content += \
            translate['Your browser does not support the video element.']
        content += '  '
        content += \
            '
'
        content += \
            ' '
    else:
        # show the responses to a question
        content += ''
    return content
def addEmojiToDisplayName(baseDir: str, httpPrefix: str,
                          nickname: str, domain: str,
                          displayName: str, inProfileName: bool) -> str:
    """Adds emoji icons to display names on individual posts
    """
    if ':' not in displayName:
        return displayName
    displayName = displayName.replace('', '').replace('
', '')
    emojiTags = {}
    print('TAG: displayName before tags: ' + displayName)
    displayName = \
        addHtmlTags(baseDir, httpPrefix,
                    nickname, domain, displayName, [], emojiTags)
    displayName = displayName.replace('', '').replace('
', '')
    print('TAG: displayName after tags: ' + displayName)
    # convert the emoji dictionary to a list
    emojiTagsList = []
    for tagName, tag in emojiTags.items():
        emojiTagsList.append(tag)
    print('TAG: emoji tags list: ' + str(emojiTagsList))
    if not inProfileName:
        displayName = \
            replaceEmojiFromTags(displayName, emojiTagsList, 'post header')
    else:
        displayName = \
            replaceEmojiFromTags(displayName, emojiTagsList, 'profile')
    print('TAG: displayName after tags 2: ' + displayName)
    # remove any stray emoji
    while ':' in displayName:
        if '://' in displayName:
            break
        emojiStr = displayName.split(':')[1]
        prevDisplayName = displayName
        displayName = displayName.replace(':' + emojiStr + ':', '').strip()
        if prevDisplayName == displayName:
            break
        print('TAG: displayName after tags 3: ' + displayName)
    print('TAG: displayName after tag replacements: ' + displayName)
    return displayName
def postContainsPublic(postJsonObject: {}) -> bool:
    """Does the given post contain #Public
    """
    containsPublic = False
    if not postJsonObject['object'].get('to'):
        return containsPublic
    for toAddress in postJsonObject['object']['to']:
        if toAddress.endswith('#Public'):
            containsPublic = True
            break
        if not containsPublic:
            if postJsonObject['object'].get('cc'):
                for toAddress in postJsonObject['object']['cc']:
                    if toAddress.endswith('#Public'):
                        containsPublic = True
                        break
    return containsPublic
def loadIndividualPostAsHtmlFromCache(baseDir: str,
                                      nickname: str, domain: str,
                                      postJsonObject: {}) -> str:
    """If a cached html version of the given post exists then load it and
    return the html text
    This is much quicker than generating the html from the json object
    """
    cachedPostFilename = \
        getCachedPostFilename(baseDir, nickname, domain, postJsonObject)
    postHtml = ''
    if not cachedPostFilename:
        return postHtml
    if not os.path.isfile(cachedPostFilename):
        return postHtml
    tries = 0
    while tries < 3:
        try:
            with open(cachedPostFilename, 'r') as file:
                postHtml = file.read()
                break
        except Exception as e:
            print(e)
            # no sleep
            tries += 1
    if postHtml:
        return postHtml
def saveIndividualPostAsHtmlToCache(baseDir: str,
                                    nickname: str, domain: str,
                                    postJsonObject: {},
                                    postHtml: str) -> bool:
    """Saves the given html for a post to a cache file
    This is so that it can be quickly reloaded on subsequent
    refresh of the timeline
    """
    htmlPostCacheDir = \
        getCachedPostDirectory(baseDir, nickname, domain)
    cachedPostFilename = \
        getCachedPostFilename(baseDir, nickname, domain, postJsonObject)
    # create the cache directory if needed
    if not os.path.isdir(htmlPostCacheDir):
        os.mkdir(htmlPostCacheDir)
    try:
        with open(cachedPostFilename, 'w') as fp:
            fp.write(postHtml)
            return True
    except Exception as e:
        print('ERROR: saving post to cache ' + str(e))
    return False
def preparePostFromHtmlCache(postHtml: str, boxName: str,
                             pageNumber: int) -> str:
    """Sets the page number on a cached html post
    """
    # if on the bookmarks timeline then remain there
    if boxName == 'tlbookmarks' or boxName == 'bookmarks':
        postHtml = postHtml.replace('?tl=inbox', '?tl=tlbookmarks')
        if '?page=' in postHtml:
            pageNumberStr = postHtml.split('?page=')[1]
            if '?' in pageNumberStr:
                pageNumberStr = pageNumberStr.split('?')[0]
            postHtml = postHtml.replace('?page=' + pageNumberStr, '?page=-999')
    withPageNumber = postHtml.replace(';-999;', ';' + str(pageNumber) + ';')
    withPageNumber = withPageNumber.replace('?page=-999',
                                            '?page=' + str(pageNumber))
    return withPageNumber
def postIsMuted(baseDir: str, nickname: str, domain: str,
                postJsonObject: {}, messageId: str) -> bool:
    """ Returns true if the given post is muted
    """
    isMuted = postJsonObject.get('muted')
    if isMuted is True or isMuted is False:
        return isMuted
    postDir = baseDir + '/accounts/' + nickname + '@' + domain
    muteFilename = \
        postDir + '/inbox/' + messageId.replace('/', '#') + '.json.muted'
    if os.path.isfile(muteFilename):
        return True
    muteFilename = \
        postDir + '/outbox/' + messageId.replace('/', '#') + '.json.muted'
    if os.path.isfile(muteFilename):
        return True
    muteFilename = \
        baseDir + '/accounts/cache/announce/' + nickname + \
        '/' + messageId.replace('/', '#') + '.json.muted'
    if os.path.isfile(muteFilename):
        return True
    return False
def getPostAttachmentsAsHtml(postJsonObject: {}, boxName: str, translate: {},
                             isMuted: bool, avatarLink: str,
                             replyStr: str, announceStr: str, likeStr: str,
                             bookmarkStr: str, deleteStr: str,
                             muteStr: str) -> (str, str):
    """Returns a string representing any attachments
    """
    attachmentStr = ''
    galleryStr = ''
    if not postJsonObject['object'].get('attachment'):
        return attachmentStr, galleryStr
    if not isinstance(postJsonObject['object']['attachment'], list):
        return attachmentStr, galleryStr
    attachmentCtr = 0
    attachmentStr += ''
    return attachmentStr, galleryStr
def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
                         iconsDir: str, translate: {},
                         pageNumber: int, baseDir: str,
                         session, wfRequest: {}, personCache: {},
                         nickname: str, domain: str, port: int,
                         postJsonObject: {},
                         avatarUrl: str, showAvatarOptions: bool,
                         allowDeletion: bool,
                         httpPrefix: str, projectVersion: str,
                         boxName: str, showRepeats=True,
                         showIcons=False,
                         manuallyApprovesFollowers=False,
                         showPublicOnly=False,
                         storeToCache=True) -> str:
    """ Shows a single post as html
    """
    postActor = postJsonObject['actor']
    # ZZZzzz
    if isPersonSnoozed(baseDir, nickname, domain, postActor):
        return ''
    avatarPosition = ''
    messageId = ''
    if postJsonObject.get('id'):
        messageId = postJsonObject['id'].replace('/activity', '')
    messageIdStr = ''
    if messageId:
        messageIdStr = ';' + messageId
    fullDomain = domain
    if port:
        if port != 80 and port != 443:
            if ':' not in domain:
                fullDomain = domain + ':' + str(port)
    pageNumberParam = ''
    if pageNumber:
        pageNumberParam = '?page=' + str(pageNumber)
    if (not showPublicOnly and
        (storeToCache or boxName == 'bookmarks' or
         boxName == 'tlbookmarks') and
       boxName != 'tlmedia'):
        # update avatar if needed
        if not avatarUrl:
            avatarUrl = \
                getPersonAvatarUrl(baseDir, postActor, personCache)
        updateAvatarImageCache(session, baseDir, httpPrefix,
                               postActor, avatarUrl, personCache)
        postHtml = \
            loadIndividualPostAsHtmlFromCache(baseDir, nickname, domain,
                                              postJsonObject)
        if postHtml:
            postHtml = preparePostFromHtmlCache(postHtml, boxName, pageNumber)
            updateRecentPostsCache(recentPostsCache, maxRecentPosts,
                                   postJsonObject, postHtml)
            return postHtml
    if not avatarUrl:
        avatarUrl = \
            getPersonAvatarUrl(baseDir, postActor, personCache)
        avatarUrl = \
            updateAvatarImageCache(session, baseDir, httpPrefix,
                                   postActor, avatarUrl, personCache)
    else:
        updateAvatarImageCache(session, baseDir, httpPrefix,
                               postActor, avatarUrl, personCache)
    if not avatarUrl:
        avatarUrl = postActor + '/avatar.png'
    if fullDomain not in postActor:
        (inboxUrl, pubKeyId, pubKey,
         fromPersonId, sharedInbox,
         capabilityAcquisition,
         avatarUrl2, displayName) = getPersonBox(baseDir, session, wfRequest,
                                                 personCache,
                                                 projectVersion, httpPrefix,
                                                 nickname, domain, 'outbox')
        if avatarUrl2:
            avatarUrl = avatarUrl2
        if displayName:
            if ':' in displayName:
                displayName = \
                    addEmojiToDisplayName(baseDir, httpPrefix,
                                          nickname, domain,
                                          displayName, False)
    avatarLink = '    '
    avatarLink += \
        '     '
    if showAvatarOptions and \
       fullDomain + '/users/' + nickname not in postActor:
        avatarLink = \
            '    '
        avatarLink += \
            '     '
    avatarImageInPost = \
        '  ' + avatarLink + '
'
    # don't create new html within the bookmarks timeline
    # it should already have been created for the inbox
    if boxName == 'tlbookmarks' or boxName == 'bookmarks':
        return ''
    timelinePostBookmark = postJsonObject['id'].replace('/activity', '')
    timelinePostBookmark = timelinePostBookmark.replace('://', '-')
    timelinePostBookmark = timelinePostBookmark.replace('/', '-')
    # If this is the inbox timeline then don't show the repeat icon on any DMs
    showRepeatIcon = showRepeats
    isPublicRepeat = False
    showDMicon = False
    if showRepeats:
        if isDM(postJsonObject):
            showDMicon = True
            showRepeatIcon = False
        else:
            if not isPublicPost(postJsonObject):
                isPublicRepeat = True
    titleStr = ''
    galleryStr = ''
    isAnnounced = False
    if postJsonObject['type'] == 'Announce':
        postJsonAnnounce = \
            downloadAnnounce(session, baseDir, httpPrefix,
                             nickname, domain, postJsonObject,
                             projectVersion)
        if not postJsonAnnounce:
            return ''
        postJsonObject = postJsonAnnounce
        isAnnounced = True
    if not isinstance(postJsonObject['object'], dict):
        return ''
    # if this post should be public then check its recipients
    if showPublicOnly:
        if not postContainsPublic(postJsonObject):
            return ''
    isModerationPost = False
    if postJsonObject['object'].get('moderationStatus'):
        isModerationPost = True
    containerClass = 'container'
    containerClassIcons = 'containericons'
    timeClass = 'time-right'
    actorNickname = getNicknameFromActor(postActor)
    if not actorNickname:
        # single user instance
        actorNickname = 'dev'
    actorDomain, actorPort = getDomainFromActor(postActor)
    displayName = getDisplayName(baseDir, postActor, personCache)
    if displayName:
        if ':' in displayName:
            displayName = \
                addEmojiToDisplayName(baseDir, httpPrefix,
                                      nickname, domain,
                                      displayName, False)
        titleStr += \
            '' + displayName + ' '
    else:
        if not messageId:
            # pprint(postJsonObject)
            print('ERROR: no messageId')
        if not actorNickname:
            # pprint(postJsonObject)
            print('ERROR: no actorNickname')
        if not actorDomain:
            # pprint(postJsonObject)
            print('ERROR: no actorDomain')
        titleStr += \
            '@' + actorNickname + '@' + actorDomain + ' '
    # Show a DM icon for DMs in the inbox timeline
    if showDMicon:
        titleStr = \
            titleStr + ' '
        else:
            if isDM(postJsonObject):
                replyStr += \
                    ' '
            else:
                replyStr += \
                    ' '
        replyStr += \
            ' '
    editStr = ''
    if fullDomain + '/users/' + nickname in postJsonObject['actor']:
        if isBlogPost(postJsonObject):
            if '/statuses/' in postJsonObject['object']['id']:
                editStr += \
                    '' + \
                    ' '
    announceStr = ''
    if not isModerationPost and showRepeatIcon:
        # don't allow announce/repeat of your own posts
        announceIcon = 'repeat_inactive.png'
        announceLink = 'repeat'
        if not isPublicRepeat:
            announceLink = 'repeatprivate'
        announceTitle = translate['Repeat this post']
        if announcedByPerson(postJsonObject, nickname, fullDomain):
            announceIcon = 'repeat.png'
            if not isPublicRepeat:
                announceLink = 'unrepeatprivate'
            announceTitle = translate['Undo the repeat']
        announceStr = \
            ''
        announceStr += \
            ' '
    likeStr = ''
    if not isModerationPost:
        likeIcon = 'like_inactive.png'
        likeLink = 'like'
        likeTitle = translate['Like this post']
        if noOfLikes(postJsonObject) > 0:
            likeIcon = 'like.png'
            if likedByPerson(postJsonObject, nickname, fullDomain):
                likeLink = 'unlike'
                likeTitle = translate['Undo the like']
        likeStr = \
            ''
        likeStr += \
            ' '
    bookmarkStr = ''
    if not isModerationPost:
        bookmarkIcon = 'bookmark_inactive.png'
        bookmarkLink = 'bookmark'
        bookmarkTitle = translate['Bookmark this post']
        if bookmarkedByPerson(postJsonObject, nickname, fullDomain):
            bookmarkIcon = 'bookmark.png'
            bookmarkLink = 'unbookmark'
            bookmarkTitle = translate['Undo the bookmark']
        bookmarkStr = \
            ''
        bookmarkStr += \
            ' '
    isMuted = postIsMuted(baseDir, nickname, domain, postJsonObject, messageId)
    deleteStr = ''
    muteStr = ''
    if (allowDeletion or
        ('/' + fullDomain + '/' in postActor and
         messageId.startswith(postActor))):
        if '/users/' + nickname + '/' in messageId:
            deleteStr = \
                ''
            deleteStr += \
                ' '
    else:
        if not isMuted:
            muteStr = \
                ''
            muteStr += \
                ' '
        else:
            muteStr = \
                ''
            muteStr += \
                ' '
    replyAvatarImageInPost = ''
    if showRepeatIcon:
        if isAnnounced:
            if postJsonObject['object'].get('attributedTo'):
                attributedTo = postJsonObject['object']['attributedTo']
                if attributedTo.startswith(postActor):
                    titleStr += \
                        ' ' + \
                                announceDisplayName + ' '
                            # show avatar of person replied to
                            announceActor = \
                                postJsonObject['object']['attributedTo']
                            announceAvatarUrl = \
                                getPersonAvatarUrl(baseDir, announceActor,
                                                   personCache)
                            if announceAvatarUrl:
                                idx = 'Show options for this person'
                                replyAvatarImageInPost = \
                                    ''
                        else:
                            titleStr += \
                                ' @' + \
                                announceNickname + '@' + \
                                announceDomain + ' '
                    else:
                        titleStr += \
                            ' @unattributed '
            else:
                titleStr += \
                    ' @unattributed '
        else:
            if postJsonObject['object'].get('inReplyTo'):
                containerClassIcons = 'containericons darker'
                containerClass = 'container darker'
                if postJsonObject['object']['inReplyTo'].startswith(postActor):
                    titleStr += \
                        ' ' + replyDisplayName + ' '
                                    # show avatar of person replied to
                                    replyAvatarUrl = \
                                        getPersonAvatarUrl(baseDir,
                                                           replyActor,
                                                           personCache)
                                    if replyAvatarUrl:
                                        replyAvatarImageInPost = \
                                            ''
                                else:
                                    inReplyTo = \
                                        postJsonObject['object']['inReplyTo']
                                    titleStr += \
                                        ' @' + \
                                        replyNickname + '@' + \
                                        replyDomain + ' '
                        else:
                            titleStr += \
                                ' @unknown '
                    else:
                        postDomain = \
                            postJsonObject['object']['inReplyTo']
                        postDomain = postDomain.replace('https://', '')
                        postDomain = postDomain.replace('http://', '')
                        postDomain = postDomain.replace('hyper://', '')
                        postDomain = postDomain.replace('dat://', '')
                        postDomain = postDomain.replace('i2p://', '')
                        if '/' in postDomain:
                            postDomain = postDomain.split('/', 1)[0]
                        if postDomain:
                            titleStr += \
                                ' ' + postDomain + ' '
    attachmentStr, galleryStr = \
        getPostAttachmentsAsHtml(postJsonObject, boxName, translate,
                                 isMuted, avatarLink,
                                 replyStr, announceStr, likeStr,
                                 bookmarkStr, deleteStr, muteStr)
    publishedStr = ''
    if postJsonObject['object'].get('published'):
        publishedStr = postJsonObject['object']['published']
        if '.' not in publishedStr:
            if '+' not in publishedStr:
                datetimeObject = \
                    datetime.strptime(publishedStr, "%Y-%m-%dT%H:%M:%SZ")
            else:
                datetimeObject = \
                    datetime.strptime(publishedStr.split('+')[0] + 'Z',
                                      "%Y-%m-%dT%H:%M:%SZ")
        else:
            publishedStr = \
                publishedStr.replace('T', ' ').split('.')[0]
            datetimeObject = parse(publishedStr)
        publishedStr = datetimeObject.strftime("%a %b %d, %H:%M")
    publishedLink = messageId
    # blog posts should have no /statuses/ in their link
    if isBlogPost(postJsonObject):
        # is this a post to the local domain?
        if '://' + domain in messageId:
            publishedLink = messageId.replace('/statuses/', '/')
    # if this is a local link then make it relative so that it works
    # on clearnet or onion address
    if domain + '/users/' in publishedLink or \
       domain + ':' + str(port) + '/users/' in publishedLink:
        publishedLink = '/users/' + publishedLink.split('/users/')[1]
    footerStr = '' + publishedStr + ' \n'
    # change the background color for DMs in inbox timeline
    if showDMicon:
        containerClassIcons = 'containericons dm'
        containerClass = 'container dm'
    if showIcons:
        footerStr = ''
        footerStr += replyStr + announceStr + likeStr + bookmarkStr + \
            deleteStr + muteStr + editStr
        footerStr += '
' + publishedStr + ' \n'
        footerStr += '
' + postJsonObject['object']['summary'] + '  '
            if isModerationPost:
                containerClass = 'container report'
        # get the content warning text
        cwContentStr = objectContent + attachmentStr
        if not isPatch:
            cwContentStr = addEmbeddedElements(translate, cwContentStr)
            cwContentStr = \
                insertQuestion(baseDir, translate, nickname, domain, port,
                               cwContentStr, postJsonObject, pageNumber)
        # get the content warning button
        contentStr += getContentWarningButton(postID, translate, cwContentStr)
    if postJsonObject['object'].get('tag') and not isPatch:
        contentStr = \
            replaceEmojiFromTags(contentStr,
                                 postJsonObject['object']['tag'],
                                 'content')
    if isMuted:
        contentStr = ''
    else:
        if not isPatch:
            contentStr = '' + contentStr + '
'
        else:
            contentStr = \
                ''
    postHtml = ''
    if boxName != 'tlmedia':
        postHtml = '\n'
        postHtml += avatarImageInPost
        postHtml += '
' + titleStr + \
            replyAvatarImageInPost + '
'
        postHtml += contentStr + footerStr
        postHtml += '
' + \
            translate['Mod'] + '  ' + translate['Shares'] + \
            '  ' + translate['Bookmarks'] + \
            '  ' + \
        translate['Switch to profile view'] + ' '
    tlStr += ''
    tlStr += '
 '
    tlStr += ''
    # 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] + ' 
 '
    if boxName == 'tlshares':
        maxSharesPerAccount = itemsPerPage
        return (tlStr +
                htmlSharesTimeline(translate, pageNumber, itemsPerPage,
                                   baseDir, actor, nickname, domain, port,
                                   maxSharesPerAccount, httpPrefix) +
                htmlFooter())
    # add the javascript for content warnings
    tlStr += ''
    # show todays events buttons on the first inbox page
    if boxName == 'inbox' and pageNumber == 1:
        if todaysEventsCheck(baseDir, nickname, domain):
            now = datetime.now()
            tlStr += \
                '' + \
                translate['Happening Today'] + ' ' + \
                    translate['Happening This Week'] + ' ' + \
                    translate['Happening This Week'] + ' \n'
        for item in timelineJson['orderedItems']:
            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 = \
                        item['id'].replace('/activity', '').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)
                if not currTlStr:
                    # read the post from disk
                    currTlStr = \
                        individualPostAsHtml(recentPostsCache, maxRecentPosts,
                                             iconsDir, translate, pageNumber,
                                             baseDir, session, wfRequest,
                                             personCache,
                                             nickname, domain, port,
                                             item, None, True,
                                             allowDeletion,
                                             httpPrefix, projectVersion,
                                             boxName,
                                             boxName != 'dm',
                                             showIndividualPostIcons,
                                             manuallyApproveFollowers,
                                             False, True)
                if currTlStr:
                    itemCtr += 1
                    tlStr += currTlStr
        if boxName == 'tlmedia':
            tlStr += '
\n'
    # page down arrow
    if itemCtr > 2:
        tlStr += \
            ''
    sharesStr += '  
'
    sharesStr += '  
'
    if sharedItemImageUrl:
        sharesStr += '  ' + translate['Remove'] + \
        ' ' + sharedItemDisplayName + ' ?
'
    sharesStr += '  '
    sharesStr += '    ' + \
        translate['Yes'] + ' '
    sharesStr += \
        '    ' + \
        translate['No'] + '  '
    sharesStr += '   '
    sharesStr += '  
'
    sharesStr += '
'
        deletePostStr += \
            '  ' + \
            translate['Delete this post?'] + '
'
        deletePostStr += '  '
        deletePostStr += \
            '    ' + \
            translate['Yes'] + ' '
        deletePostStr += \
            '    ' + \
            translate['No'] + '  '
        deletePostStr += ' '
        deletePostStr += htmlFooter()
    return deletePostStr
def htmlCalendarDeleteConfirm(translate: {}, baseDir: str,
                              path: str, httpPrefix: str,
                              domainFull: str, postId: str, postTime: str,
                              year: int, monthNumber: int,
                              dayNumber: int) -> str:
    """Shows a screen asking to confirm the deletion of a calendar event
    """
    nickname = getNicknameFromActor(path)
    actor = httpPrefix + '://' + domainFull + '/users/' + nickname
    domain, port = getDomainFromActor(actor)
    messageId = actor + '/statuses/' + postId
    postFilename = locatePost(baseDir, nickname, domain, messageId)
    if not postFilename:
        return None
    postJsonObject = loadJson(postFilename)
    if not postJsonObject:
        return None
    if os.path.isfile(baseDir + '/img/delete-background.png'):
        if not os.path.isfile(baseDir + '/accounts/delete-background.png'):
            copyfile(baseDir + '/img/delete-background.png',
                     baseDir + '/accounts/delete-background.png')
    deletePostStr = None
    cssFilename = baseDir + '/epicyon-profile.css'
    if os.path.isfile(baseDir + '/epicyon.css'):
        cssFilename = baseDir + '/epicyon.css'
    with open(cssFilename, 'r') as cssFile:
        profileStyle = cssFile.read()
        if httpPrefix != 'https':
            profileStyle = profileStyle.replace('https://',
                                                httpPrefix + '://')
        deletePostStr = htmlHeader(cssFilename, profileStyle)
        deletePostStr += \
            '' + postTime + ' ' + str(year) + '/' + \
            str(monthNumber) + \
            '/' + str(dayNumber) + ' '
        deletePostStr += '  ' + \
            translate['Delete this event'] + '
'
        deletePostStr += '  '
        deletePostStr += '    ' + \
            translate['Yes'] + ' '
        deletePostStr += \
            '    ' + \
            translate['No'] + '  '
        deletePostStr += ' '
        deletePostStr += htmlFooter()
    return deletePostStr
def htmlFollowConfirm(translate: {}, baseDir: str,
                      originPathStr: str,
                      followActor: str,
                      followProfileUrl: str) -> str:
    """Asks to confirm a follow
    """
    followDomain, port = getDomainFromActor(followActor)
    if os.path.isfile(baseDir + '/img/follow-background.png'):
        if not os.path.isfile(baseDir + '/accounts/follow-background.png'):
            copyfile(baseDir + '/img/follow-background.png',
                     baseDir + '/accounts/follow-background.png')
    cssFilename = baseDir + '/epicyon-follow.css'
    if os.path.isfile(baseDir + '/follow.css'):
        cssFilename = baseDir + '/follow.css'
    with open(cssFilename, 'r') as cssFile:
        profileStyle = cssFile.read()
    followStr = htmlHeader(cssFilename, profileStyle)
    followStr += ''
    followStr += '  
'
    followStr += '  
'
    followStr += '  '
    followStr += '   '
    followStr += \
        '  ' + translate['Follow'] + ' ' + \
        getNicknameFromActor(followActor) + '@' + followDomain + ' ?
'
    followStr += '  '
    followStr += '    ' + \
        translate['Yes'] + ' '
    followStr += \
        '    ' + \
        translate['No'] + '  '
    followStr += ' '
    followStr += '
'
    followStr += '
'
    followStr += '  
'
    followStr += '  
'
    followStr += '  '
    followStr += '   '
    followStr += \
        '  ' + translate['Stop following'] + \
        ' ' + getNicknameFromActor(followActor) + '@' + followDomain + ' ?
'
    followStr += '  '
    followStr += '    ' + \
        translate['Yes'] + ' '
    followStr += \
        '    ' + \
        translate['No'] + '  '
    followStr += ' '
    followStr += '
'
    followStr += '
' + \
            translate['Donate'] + ' '
    optionsStr += '  
'
    optionsStr += '  
'
    optionsStr += '  '
    optionsStr += '   '
    optionsStr += \
        '  ' + translate['Options for'] + \
        ' @' + getNicknameFromActor(optionsActor) + '@' + \
        optionsDomain + '
'
    if emailAddress:
        optionsStr += \
            '' + translate['Email'] + \
            ': ' + emailAddress + ' 
'
    if xmppAddress:
        optionsStr += \
            '' + translate['XMPP'] + \
            ': ' + xmppAddress + ' 
'
    if matrixAddress:
        optionsStr += \
            '' + translate['Matrix'] + ': ' + \
            matrixAddress + '
'
    if ssbAddress:
        optionsStr += \
            'SSB: ' + ssbAddress + '
'
    if blogAddress:
        optionsStr += \
            'Blog: ' + \
            blogAddress + ' 
'
    if toxAddress:
        optionsStr += \
            'Tox: ' + toxAddress + '
'
    if PGPpubKey:
        optionsStr += '' + \
            PGPpubKey.replace('\n', '
'
    optionsStr += '  '
    optionsStr += '    ' + \
        translate['View'] + ' '
    optionsStr += donateStr
    optionsStr += \
        '    ' + translate[followStr] + ' '
    optionsStr += \
        '    ' + translate[blockStr] + ' '
    optionsStr += \
        '    ' + \
        translate['DM'] + ' '
    optionsStr += \
        '    ' + translate[snoozeButtonStr] + ' '
    optionsStr += \
        '    ' + \
        translate['Report'] + ' '
    optionsStr += '   '
    optionsStr += ' '
    optionsStr += '
'
    optionsStr += '
'
    blockStr += '  
'
    blockStr += '  
'
    blockStr += '  '
    blockStr += '   '
    blockStr += \
        '  ' + translate['Stop blocking'] + ' ' + \
        getNicknameFromActor(blockActor) + '@' + blockDomain + ' ?
'
    blockStr += '  '
    blockStr += '    ' + \
        translate['Yes'] + ' '
    blockStr += \
        '    ' + \
        translate['No'] + '  '
    blockStr += ' '
    blockStr += '
'
    blockStr += '
'
    emojiStr += '  
'
    emojiStr += '  
'
    emojiStr += \
        '  ' + \
        translate['Enter an emoji name to search for'] + '
'
    emojiStr += '  '
    emojiStr += '    ' + \
        translate['Submit'] + ' '
    emojiStr += '   '
    emojiStr += '   '
    emojiStr += '  
'
    emojiStr += '
\n'
    calendarStr += '\n'
    calendarStr += \
        '  '
    calendarStr += \
        '  ' + str(dayNumber) + ' ' + monthName + \
        '  ' + str(year) + ' \n'
    calendarStr += ' \n'
    calendarStr += '\n'
    iconsDir = getIconsDir(baseDir)
    if dayEvents:
        for eventPost in dayEvents:
            eventTime = None
            eventDescription = None
            eventPlace = None
            postId = None
            # get the time place and description
            for ev in eventPost:
                if ev['type'] == 'Event':
                    if ev.get('postId'):
                        postId = ev['postId']
                    if ev.get('startTime'):
                        eventDate = \
                            datetime.strptime(ev['startTime'],
                                              "%Y-%m-%dT%H:%M:%S%z")
                        eventTime = eventDate.strftime("%H:%M").strip()
                    if ev.get('name'):
                        eventDescription = ev['name'].strip()
                elif ev['type'] == 'Place':
                    if ev.get('name'):
                        eventPlace = ev['name']
            deleteButtonStr = ''
            if postId:
                deleteButtonStr = \
                    '' + eventTime + \
                    ' ' + \
                    '' + \
                    eventPlace + '  ' + deleteButtonStr + '' + eventTime + \
                    ' ' + \
                    eventDescription + ' ' + deleteButtonStr + '' + \
                    ' ' + \
                    eventDescription + ' ' + deleteButtonStr + '' + \
                    eventPlace + ' ' + eventTime + \
                    ' ' + \
                    '' + \
                    eventPlace + '  ' + \
                    deleteButtonStr + ' \n'
    calendarStr += '
\n'
    calendarStr += '\n'
    calendarStr += \
        '  '
    calendarStr += \
        '   \n'
    calendarStr += '  '
    calendarStr += '  ' + monthName + '  \n'
    calendarStr += \
        '  '
    calendarStr += \
        '   \n'
    calendarStr += ' \n'
    calendarStr += '\n'
    calendarStr += '\n'
    calendarStr += '  \n'
    calendarStr += '  \n'
    calendarStr += '  \n'
    calendarStr += '  \n'
    calendarStr += '  \n'
    calendarStr += '  \n'
    calendarStr += '  \n'
    calendarStr += ' \n'
    calendarStr += ' \n'
    calendarStr += '\n'
    dayOfMonth = 0
    dow = weekDayOfMonthStart(monthNumber, year)
    for weekOfMonth in range(1, 6):
        calendarStr += '  \n'
        for dayNumber in range(1, 8):
            if (weekOfMonth > 1 and dayOfMonth < daysInMonth) or \
               (weekOfMonth == 1 and dayNumber >= dow):
                dayOfMonth += 1
                isToday = False
                if year == currDate.year:
                    if currDate.month == monthNumber:
                        if dayOfMonth == currDate.day:
                            isToday = True
                if events.get(str(dayOfMonth)):
                    url = actor + '/calendar?year=' + str(year) + '?month=' + \
                        str(monthNumber) + '?day=' + str(dayOfMonth)
                    dayLink = '' + \
                        str(dayOfMonth) + ' '
                    # there are events for this day
                    if not isToday:
                        calendarStr += \
                            '    ' + \
                            dayLink + ' \n'
                    else:
                        calendarStr += \
                            '    ' + \
                            dayLink + ' \n'
                else:
                    # No events today
                    if not isToday:
                        calendarStr += \
                            '    ' + \
                            str(dayOfMonth) + ' \n'
                    else:
                        calendarStr += \
                            '    ' + str(dayOfMonth) + ' \n'
            else:
                calendarStr += '     \n'
    calendarStr += ' \n'
    calendarStr += '
' + tagName + '  '
        ctr += 1
    tagSwarmHtml = tagSwarmStr.strip() + '\n'
    return tagSwarmHtml
def htmlSearch(translate: {},
               baseDir: str, path: str) -> str:
    """Search called from the timeline icon
    """
    actor = path.replace('/search', '')
    domain, port = getDomainFromActor(actor)
    if os.path.isfile(baseDir + '/img/search-background.png'):
        if not os.path.isfile(baseDir + '/accounts/search-background.png'):
            copyfile(baseDir + '/img/search-background.png',
                     baseDir + '/accounts/search-background.png')
    cssFilename = baseDir + '/epicyon-follow.css'
    if os.path.isfile(baseDir + '/follow.css'):
        cssFilename = baseDir + '/follow.css'
    with open(cssFilename, 'r') as cssFile:
        profileStyle = cssFile.read()
    followStr = htmlHeader(cssFilename, profileStyle)
    followStr += ''
    followStr += '  
'
    followStr += '  
'
    idx = 'Enter an address, shared item, !history, #hashtag, ' + \
        '*skill or :emoji: to search for'
    followStr += \
        '  ' + translate[idx] + '
'
    followStr += '  '
    followStr += '    ' + translate['Submit'] + ' '
    followStr += '    ' + translate['Go Back'] + ' '
    followStr += '   '
    followStr += '  ' + \
        htmlHashTagSwarm(baseDir, actor) + '
'
    followStr += '   '
    followStr += '  
'
    followStr += '
', '')
            avatarDescription = avatarDescription.replace('
', '')
        profileStr = ' '
        profileStr += '  
'
        profileStr += \
            '    
'
        profileStr += '    
' + displayName + ' '
        profileStr += '    
@' + searchNickname + '@' + \
            searchDomainFull + ' 
'
        profileStr += '    
' + profileDescriptionShort + '
'
        profileStr += '  
'
        profileStr += '
\n'
        profileStr += '  
'
        profileStr += '    '
        profileStr += \
            '      ' + \
            translate['Follow'] + ' '
        profileStr += \
            '      ' + \
            translate['View'] + ' '
        profileStr += \
            '      ' + \
            translate['Go Back'] + '  '
        profileStr += '   '
        profileStr += '