Split up webapp into smaller modules

main
Bob Mottram 2020-11-09 22:44:03 +00:00
parent 4a3a2c2319
commit 2f82d2281c
9 changed files with 4278 additions and 4168 deletions

View File

@ -114,46 +114,46 @@ from blog import htmlBlogPage
from blog import htmlBlogPost
from blog import htmlEditBlog
from webapp_utils import setBlogAddress
from webapp_utils import getBlogAddress
from webapp_calendar import htmlCalendarDeleteConfirm
from webapp_calendar import htmlCalendar
from webapp import htmlCitations
from webapp import htmlFollowingList
from webapp import getBlogAddress
from webapp import htmlDeletePost
from webapp import htmlAbout
from webapp import htmlRemoveSharedItem
from webapp import htmlInboxDMs
from webapp import htmlInboxReplies
from webapp import htmlInboxMedia
from webapp import htmlInboxBlogs
from webapp import htmlInboxNews
from webapp import htmlUnblockConfirm
from webapp import htmlPersonOptions
from webapp import htmlIndividualPost
from webapp import htmlProfile
from webapp import htmlInbox
from webapp import htmlBookmarks
from webapp import htmlEvents
from webapp import htmlShares
from webapp import htmlOutbox
from webapp import htmlModeration
from webapp import htmlPostReplies
from webapp_person_options import htmlPersonOptions
from webapp_timeline import htmlShares
from webapp_timeline import htmlInbox
from webapp_timeline import htmlBookmarks
from webapp_timeline import htmlEvents
from webapp_timeline import htmlInboxDMs
from webapp_timeline import htmlInboxReplies
from webapp_timeline import htmlInboxMedia
from webapp_timeline import htmlInboxBlogs
from webapp_timeline import htmlInboxNews
from webapp_timeline import htmlOutbox
from webapp_timeline import htmlModeration
from webapp import htmlLogin
from webapp import htmlSuspended
from webapp import htmlGetLoginCredentials
from webapp import htmlNewPost
from webapp import htmlFollowConfirm
from webapp import htmlNewswireMobile
from webapp import htmlLinksMobile
from webapp import htmlUnfollowConfirm
from webapp import htmlProfileAfterSearch
from webapp import htmlEditProfile
from webapp import htmlEditLinks
from webapp import htmlEditNewswire
from webapp import htmlEditNewsPost
from webapp import htmlTermsOfService
from webapp import htmlModerationInfo
from webapp import htmlHashtagBlocked
from webapp_post import htmlPostReplies
from webapp_post import htmlIndividualPost
from webapp_profile import htmlEditProfile
from webapp_profile import htmlProfileAfterSearch
from webapp_profile import htmlProfile
from webapp_column_left import htmlLinksMobile
from webapp_column_left import htmlEditLinks
from webapp_column_right import htmlNewswireMobile
from webapp_column_right import htmlEditNewswire
from webapp_column_right import htmlCitations
from webapp_search import htmlSkillsSearch
from webapp_search import htmlHistorySearch
from webapp_search import htmlHashtagSearch

4145
webapp.py

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,317 @@
__filename__ = "webapp_column_left.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.1.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
import os
from shutil import copyfile
from utils import getConfigParam
from utils import getCSS
from utils import getNicknameFromActor
from posts import isEditor
from webapp_utils import htmlPostSeparator
from webapp_utils import getLeftImageFile
from webapp_utils import getImageFile
from webapp_utils import headerButtonsFrontScreen
from webapp_utils import getIconsDir
from webapp_utils import htmlHeader
from webapp_utils import htmlFooter
from webapp_utils import getBannerFile
def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str,
httpPrefix: str, translate: {},
iconsDir: str, editor: bool,
showBackButton: bool, timelinePath: str,
rssIconAtTop: bool, showHeaderImage: bool,
frontPage: bool) -> str:
"""Returns html content for the left column
"""
htmlStr = ''
separatorStr = htmlPostSeparator(baseDir, 'left')
domain = domainFull
if ':' in domain:
domain = domain.split(':')
editImageClass = ''
if showHeaderImage:
leftImageFile, leftColumnImageFilename = \
getLeftImageFile(baseDir, nickname, domain)
if not os.path.isfile(leftColumnImageFilename):
theme = getConfigParam(baseDir, 'theme').lower()
if theme == 'default':
theme = ''
else:
theme = '_' + theme
themeLeftImageFile, themeLeftColumnImageFilename = \
getImageFile(baseDir, 'left_col_image', baseDir + '/img',
nickname, domain)
if os.path.isfile(themeLeftColumnImageFilename):
leftColumnImageFilename = \
baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + themeLeftImageFile
copyfile(themeLeftColumnImageFilename,
leftColumnImageFilename)
leftImageFile = themeLeftImageFile
# show the image at the top of the column
editImageClass = 'leftColEdit'
if os.path.isfile(leftColumnImageFilename):
editImageClass = 'leftColEditImage'
htmlStr += \
'\n <center>\n' + \
' <img class="leftColImg" ' + \
'loading="lazy" src="/users/' + \
nickname + '/' + leftImageFile + '" />\n' + \
' </center>\n'
if showBackButton:
htmlStr += \
' <div>' + \
' <a href="' + timelinePath + '">' + \
'<button class="cancelbtn">' + \
translate['Go Back'] + '</button></a>\n'
if (editor or rssIconAtTop) and not showHeaderImage:
htmlStr += '<div class="columnIcons">'
if editImageClass == 'leftColEdit':
htmlStr += '\n <center>\n'
htmlStr += ' <div class="leftColIcons">\n'
if editor:
# show the edit icon
htmlStr += \
' <a href="' + \
'/users/' + nickname + '/editlinks">' + \
'<img class="' + editImageClass + \
'" loading="lazy" alt="' + \
translate['Edit Links'] + '" title="' + \
translate['Edit Links'] + '" src="/' + \
iconsDir + '/edit.png" /></a>\n'
# RSS icon
if nickname != 'news':
# rss feed for this account
rssUrl = httpPrefix + '://' + domainFull + \
'/blog/' + nickname + '/rss.xml'
else:
# rss feed for all accounts on the instance
rssUrl = httpPrefix + '://' + domainFull + '/blog/rss.xml'
if not frontPage:
rssTitle = translate['RSS feed for your blog']
else:
rssTitle = translate['RSS feed for this site']
rssIconStr = \
' <a href="' + rssUrl + '">' + \
'<img class="' + editImageClass + \
'" loading="lazy" alt="' + rssTitle + \
'" title="' + rssTitle + \
'" src="/' + iconsDir + '/logorss.png" /></a>\n'
if rssIconAtTop:
htmlStr += rssIconStr
htmlStr += ' </div>\n'
if editImageClass == 'leftColEdit':
htmlStr += ' </center>\n'
if (editor or rssIconAtTop) and not showHeaderImage:
htmlStr += '</div><br>'
# if showHeaderImage:
# htmlStr += '<br>'
linksFilename = baseDir + '/accounts/links.txt'
linksFileContainsEntries = False
if os.path.isfile(linksFilename):
linksList = None
with open(linksFilename, "r") as f:
linksList = f.readlines()
if linksList:
for lineStr in linksList:
if ' ' not in lineStr:
if '#' not in lineStr:
if '*' not in lineStr:
continue
lineStr = lineStr.strip()
words = lineStr.split(' ')
# get the link
linkStr = None
for word in words:
if word == '#':
continue
if word == '*':
continue
if '://' in word:
linkStr = word
break
if linkStr:
lineStr = lineStr.replace(linkStr, '').strip()
# avoid any dubious scripts being added
if '<' not in lineStr:
# remove trailing comma if present
if lineStr.endswith(','):
lineStr = lineStr[:len(lineStr)-1]
# add link to the returned html
htmlStr += \
' <p><a href="' + linkStr + '">' + \
lineStr + '</a></p>\n'
linksFileContainsEntries = True
else:
if lineStr.startswith('#') or lineStr.startswith('*'):
lineStr = lineStr[1:].strip()
htmlStr += separatorStr
htmlStr += \
' <h3 class="linksHeader">' + \
lineStr + '</h3>\n'
else:
htmlStr += \
' <p>' + lineStr + '</p>\n'
linksFileContainsEntries = True
if linksFileContainsEntries and not rssIconAtTop:
htmlStr += '<br><div class="columnIcons">' + rssIconStr + '</div>'
return htmlStr
def htmlLinksMobile(cssCache: {}, baseDir: str,
nickname: str, domainFull: str,
httpPrefix: str, translate,
timelinePath: str, authorized: bool,
rssIconAtTop: bool,
iconsAsButtons: bool,
defaultTimeline: str) -> str:
"""Show the left column links within mobile view
"""
htmlStr = ''
# the css filename
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
profileStyle = getCSS(baseDir, cssFilename, cssCache)
if profileStyle:
# replace any https within the css with whatever prefix is needed
if httpPrefix != 'https':
profileStyle = \
profileStyle.replace('https://', httpPrefix + '://')
iconsDir = getIconsDir(baseDir)
# is the user a site editor?
if nickname == 'news':
editor = False
else:
editor = isEditor(baseDir, nickname)
domain = domainFull
if ':' in domain:
domain = domain.split(':')[0]
htmlStr = htmlHeader(cssFilename, profileStyle)
bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain)
htmlStr += \
'<a href="/users/' + nickname + '/' + defaultTimeline + '">' + \
'<img loading="lazy" class="timeline-banner" ' + \
'src="/users/' + nickname + '/' + bannerFile + '" /></a>\n'
htmlStr += '<center>' + \
headerButtonsFrontScreen(translate, nickname,
'links', authorized,
iconsAsButtons, iconsDir) + '</center>'
htmlStr += \
getLeftColumnContent(baseDir, nickname, domainFull,
httpPrefix, translate,
iconsDir, editor,
False, timelinePath,
rssIconAtTop, False, False)
htmlStr += '</div>\n' + htmlFooter()
return htmlStr
def htmlEditLinks(cssCache: {}, translate: {}, baseDir: str, path: str,
domain: str, port: int, httpPrefix: str,
defaultTimeline: str) -> str:
"""Shows the edit links screen
"""
if '/users/' not in path:
return ''
path = path.replace('/inbox', '').replace('/outbox', '')
path = path.replace('/shares', '')
nickname = getNicknameFromActor(path)
if not nickname:
return ''
# is the user a moderator?
if not isEditor(baseDir, nickname):
return ''
cssFilename = baseDir + '/epicyon-links.css'
if os.path.isfile(baseDir + '/links.css'):
cssFilename = baseDir + '/links.css'
editCSS = getCSS(baseDir, cssFilename, cssCache)
if editCSS:
if httpPrefix != 'https':
editCSS = \
editCSS.replace('https://', httpPrefix + '://')
# filename of the banner shown at the top
bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain)
editLinksForm = htmlHeader(cssFilename, editCSS)
# top banner
editLinksForm += \
'<a href="/users/' + nickname + '/' + defaultTimeline + '" title="' + \
translate['Switch to timeline view'] + '" alt="' + \
translate['Switch to timeline view'] + '">\n'
editLinksForm += '<img loading="lazy" class="timeline-banner" src="' + \
'/users/' + nickname + '/' + bannerFile + '" /></a>\n'
editLinksForm += \
'<form enctype="multipart/form-data" method="POST" ' + \
'accept-charset="UTF-8" action="' + path + '/linksdata">\n'
editLinksForm += \
' <div class="vertical-center">\n'
editLinksForm += \
' <p class="new-post-text">' + translate['Edit Links'] + '</p>'
editLinksForm += \
' <div class="container">\n'
# editLinksForm += \
# ' <a href="' + pathOriginal + '"><button class="cancelbtn">' + \
# translate['Go Back'] + '</button></a>\n'
editLinksForm += \
' <center>\n' + \
' <input type="submit" name="submitLinks" value="' + \
translate['Submit'] + '">\n' + \
' </center>\n'
editLinksForm += \
' </div>\n'
linksFilename = baseDir + '/accounts/links.txt'
linksStr = ''
if os.path.isfile(linksFilename):
with open(linksFilename, 'r') as fp:
linksStr = fp.read()
editLinksForm += \
'<div class="container">'
editLinksForm += \
' ' + \
translate['One link per line. Description followed by the link.'] + \
'<br>'
editLinksForm += \
' <textarea id="message" name="editedLinks" style="height:80vh">' + \
linksStr + '</textarea>'
editLinksForm += \
'</div>'
editLinksForm += htmlFooter()
return editLinksForm

View File

@ -0,0 +1,567 @@
__filename__ = "webapp_column_right.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.1.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
import os
from datetime import datetime
from shutil import copyfile
from content import removeLongWords
from utils import getCSS
from utils import getConfigParam
from utils import votesOnNewswireItem
from utils import getNicknameFromActor
from posts import isEditor
from posts import isModerator
from webapp_utils import getRightImageFile
from webapp_utils import getImageFile
from webapp_utils import htmlHeader
from webapp_utils import htmlFooter
from webapp_utils import getBannerFile
from webapp_utils import htmlPostSeparator
from webapp_utils import headerButtonsFrontScreen
from webapp_utils import getIconsDir
def votesIndicator(totalVotes: int, positiveVoting: bool) -> str:
"""Returns an indicator of the number of votes on a newswire item
"""
if totalVotes <= 0:
return ''
totalVotesStr = ' '
for v in range(totalVotes):
if positiveVoting:
totalVotesStr += ''
else:
totalVotesStr += ''
return totalVotesStr
def getRightColumnContent(baseDir: str, nickname: str, domainFull: str,
httpPrefix: str, translate: {},
iconsDir: str, moderator: bool, editor: bool,
newswire: {}, positiveVoting: bool,
showBackButton: bool, timelinePath: str,
showPublishButton: bool,
showPublishAsIcon: bool,
rssIconAtTop: bool,
publishButtonAtTop: bool,
authorized: bool,
showHeaderImage: bool) -> str:
"""Returns html content for the right column
"""
htmlStr = ''
domain = domainFull
if ':' in domain:
domain = domain.split(':')
if authorized:
# only show the publish button if logged in, otherwise replace it with
# a login button
publishButtonStr = \
' <a href="' + \
'/users/' + nickname + '/newblog" ' + \
'title="' + translate['Publish a news article'] + '">' + \
'<button class="publishbtn">' + \
translate['Publish'] + '</button></a>\n'
else:
# if not logged in then replace the publish button with
# a login button
publishButtonStr = \
' <a href="/login"><button class="publishbtn">' + \
translate['Login'] + '</button></a>\n'
# show publish button at the top if needed
if publishButtonAtTop:
htmlStr += '<center>' + publishButtonStr + '</center>'
# show a column header image, eg. title of the theme or newswire banner
editImageClass = ''
if showHeaderImage:
rightImageFile, rightColumnImageFilename = \
getRightImageFile(baseDir, nickname, domain)
if not os.path.isfile(rightColumnImageFilename):
theme = getConfigParam(baseDir, 'theme').lower()
if theme == 'default':
theme = ''
else:
theme = '_' + theme
themeRightImageFile, themeRightColumnImageFilename = \
getImageFile(baseDir, 'right_col_image', baseDir + '/img',
nickname, domain)
if os.path.isfile(themeRightColumnImageFilename):
rightColumnImageFilename = \
baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + themeRightImageFile
copyfile(themeRightColumnImageFilename,
rightColumnImageFilename)
rightImageFile = themeRightImageFile
# show the image at the top of the column
editImageClass = 'rightColEdit'
if os.path.isfile(rightColumnImageFilename):
editImageClass = 'rightColEditImage'
htmlStr += \
'\n <center>\n' + \
' <img class="rightColImg" ' + \
'loading="lazy" src="/users/' + \
nickname + '/' + rightImageFile + '" />\n' + \
' </center>\n'
if (showPublishButton or editor or rssIconAtTop) and not showHeaderImage:
htmlStr += '<div class="columnIcons">'
if editImageClass == 'rightColEdit':
htmlStr += '\n <center>\n'
# whether to show a back icon
# This is probably going to be osolete soon
if showBackButton:
htmlStr += \
' <a href="' + timelinePath + '">' + \
'<button class="cancelbtn">' + \
translate['Go Back'] + '</button></a>\n'
if showPublishButton and not publishButtonAtTop:
if not showPublishAsIcon:
htmlStr += publishButtonStr
# show the edit icon
if editor:
if os.path.isfile(baseDir + '/accounts/newswiremoderation.txt'):
# show the edit icon highlighted
htmlStr += \
' <a href="' + \
'/users/' + nickname + '/editnewswire">' + \
'<img class="' + editImageClass + \
'" loading="lazy" alt="' + \
translate['Edit newswire'] + '" title="' + \
translate['Edit newswire'] + '" src="/' + \
iconsDir + '/edit_notify.png" /></a>\n'
else:
# show the edit icon
htmlStr += \
' <a href="' + \
'/users/' + nickname + '/editnewswire">' + \
'<img class="' + editImageClass + \
'" loading="lazy" alt="' + \
translate['Edit newswire'] + '" title="' + \
translate['Edit newswire'] + '" src="/' + \
iconsDir + '/edit.png" /></a>\n'
# show the RSS icon
rssIconStr = \
' <a href="/newswire.xml">' + \
'<img class="' + editImageClass + \
'" loading="lazy" alt="' + \
translate['Newswire RSS Feed'] + '" title="' + \
translate['Newswire RSS Feed'] + '" src="/' + \
iconsDir + '/logorss.png" /></a>\n'
if rssIconAtTop:
htmlStr += rssIconStr
# show publish icon at top
if showPublishButton:
if showPublishAsIcon:
htmlStr += \
' <a href="' + \
'/users/' + nickname + '/newblog">' + \
'<img class="' + editImageClass + \
'" loading="lazy" alt="' + \
translate['Publish a news article'] + '" title="' + \
translate['Publish a news article'] + '" src="/' + \
iconsDir + '/publish.png" /></a>\n'
if editImageClass == 'rightColEdit':
htmlStr += ' </center>\n'
else:
if showHeaderImage:
htmlStr += ' <br>\n'
if (showPublishButton or editor or rssIconAtTop) and not showHeaderImage:
htmlStr += '</div><br>'
# show the newswire lines
newswireContentStr = \
htmlNewswire(baseDir, newswire, nickname, moderator, translate,
positiveVoting, iconsDir)
htmlStr += newswireContentStr
# show the rss icon at the bottom, typically on the right hand side
if newswireContentStr and not rssIconAtTop:
htmlStr += '<br><div class="columnIcons">' + rssIconStr + '</div>'
return htmlStr
def htmlNewswire(baseDir: str, newswire: {}, nickname: str, moderator: bool,
translate: {}, positiveVoting: bool, iconsDir: str) -> str:
"""Converts a newswire dict into html
"""
separatorStr = htmlPostSeparator(baseDir, 'right')
htmlStr = ''
for dateStr, item in newswire.items():
publishedDate = \
datetime.strptime(dateStr, "%Y-%m-%d %H:%M:%S%z")
dateShown = publishedDate.strftime("%Y-%m-%d %H:%M")
dateStrLink = dateStr.replace('T', ' ')
dateStrLink = dateStrLink.replace('Z', '')
moderatedItem = item[5]
htmlStr += separatorStr
if moderatedItem and 'vote:' + nickname in item[2]:
totalVotesStr = ''
totalVotes = 0
if moderator:
totalVotes = votesOnNewswireItem(item[2])
totalVotesStr = \
votesIndicator(totalVotes, positiveVoting)
title = removeLongWords(item[0], 16, []).replace('\n', '<br>')
htmlStr += '<p class="newswireItemVotedOn">' + \
'<a href="' + item[1] + '">' + \
'<span class="newswireItemVotedOn">' + title + \
'</span></a>' + totalVotesStr
if moderator:
htmlStr += \
' ' + dateShown + '<a href="/users/' + nickname + \
'/newswireunvote=' + dateStrLink + '" ' + \
'title="' + translate['Remove Vote'] + '">'
htmlStr += '<img loading="lazy" class="voteicon" src="/' + \
iconsDir + '/vote.png" /></a></p>\n'
else:
htmlStr += ' <span class="newswireDateVotedOn">'
htmlStr += dateShown + '</span></p>\n'
else:
totalVotesStr = ''
totalVotes = 0
if moderator:
if moderatedItem:
totalVotes = votesOnNewswireItem(item[2])
# show a number of ticks or crosses for how many
# votes for or against
totalVotesStr = \
votesIndicator(totalVotes, positiveVoting)
title = removeLongWords(item[0], 16, []).replace('\n', '<br>')
if moderator and moderatedItem:
htmlStr += '<p class="newswireItemModerated">' + \
'<a href="' + item[1] + '">' + \
title + '</a>' + totalVotesStr
htmlStr += ' ' + dateShown
htmlStr += '<a href="/users/' + nickname + \
'/newswirevote=' + dateStrLink + '" ' + \
'title="' + translate['Vote'] + '">'
htmlStr += '<img class="voteicon" src="/' + \
iconsDir + '/vote.png" /></a>'
htmlStr += '</p>\n'
else:
htmlStr += '<p class="newswireItem">' + \
'<a href="' + item[1] + '">' + \
title + '</a>' + \
totalVotesStr
htmlStr += ' <span class="newswireDate">'
htmlStr += dateShown + '</span></p>\n'
return htmlStr
def htmlCitations(baseDir: str, nickname: str, domain: str,
httpPrefix: str, defaultTimeline: str,
translate: {}, newswire: {}, cssCache: {},
blogTitle: str, blogContent: str,
blogImageFilename: str,
blogImageAttachmentMediaType: str,
blogImageDescription: str) -> str:
"""Show the citations screen when creating a blog
"""
htmlStr = ''
# create a list of dates for citations
# these can then be used to re-select checkboxes later
citationsFilename = \
baseDir + '/accounts/' + \
nickname + '@' + domain + '/.citations.txt'
citationsSelected = []
if os.path.isfile(citationsFilename):
citationsSeparator = '#####'
with open(citationsFilename, "r") as f:
citations = f.readlines()
for line in citations:
if citationsSeparator not in line:
continue
sections = line.strip().split(citationsSeparator)
if len(sections) != 3:
continue
dateStr = sections[0]
citationsSelected.append(dateStr)
# the css filename
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
profileStyle = getCSS(baseDir, cssFilename, cssCache)
if profileStyle:
# replace any https within the css with whatever prefix is needed
if httpPrefix != 'https':
profileStyle = \
profileStyle.replace('https://', httpPrefix + '://')
# iconsDir = getIconsDir(baseDir)
htmlStr = htmlHeader(cssFilename, profileStyle)
# top banner
bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain)
htmlStr += \
'<a href="/users/' + nickname + '/newblog" title="' + \
translate['Go Back'] + '" alt="' + \
translate['Go Back'] + '">\n'
htmlStr += '<img loading="lazy" class="timeline-banner" src="' + \
'/users/' + nickname + '/' + bannerFile + '" /></a>\n'
htmlStr += \
'<form enctype="multipart/form-data" method="POST" ' + \
'accept-charset="UTF-8" action="/users/' + nickname + \
'/citationsdata">\n'
htmlStr += ' <center>\n'
htmlStr += translate['Choose newswire items ' +
'referenced in your article'] + '<br>'
if blogTitle is None:
blogTitle = ''
htmlStr += \
' <input type="hidden" name="blogTitle" value="' + \
blogTitle + '">\n'
if blogContent is None:
blogContent = ''
htmlStr += \
' <input type="hidden" name="blogContent" value="' + \
blogContent + '">\n'
# submit button
htmlStr += \
' <input type="submit" name="submitCitations" value="' + \
translate['Submit'] + '">\n'
htmlStr += ' </center>\n'
citationsSeparator = '#####'
# list of newswire items
if newswire:
ctr = 0
for dateStr, item in newswire.items():
# should this checkbox be selected?
selectedStr = ''
if dateStr in citationsSelected:
selectedStr = ' checked'
publishedDate = \
datetime.strptime(dateStr, "%Y-%m-%d %H:%M:%S%z")
dateShown = publishedDate.strftime("%Y-%m-%d %H:%M")
title = removeLongWords(item[0], 16, []).replace('\n', '<br>')
link = item[1]
citationValue = \
dateStr + citationsSeparator + \
title + citationsSeparator + \
link
htmlStr += \
'<input type="checkbox" name="newswire' + str(ctr) + \
'" value="' + citationValue + '"' + selectedStr + '/>' + \
'<a href="' + link + '"><cite>' + title + '</cite></a> '
htmlStr += '<span class="newswireDate">' + \
dateShown + '</span><br>\n'
ctr += 1
htmlStr += '</form>\n'
return htmlStr + htmlFooter()
def htmlNewswireMobile(cssCache: {}, baseDir: str, nickname: str,
domain: str, domainFull: str,
httpPrefix: str, translate: {},
newswire: {},
positiveVoting: bool,
timelinePath: str,
showPublishAsIcon: bool,
authorized: bool,
rssIconAtTop: bool,
iconsAsButtons: bool,
defaultTimeline: str) -> str:
"""Shows the mobile version of the newswire right column
"""
htmlStr = ''
# the css filename
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
profileStyle = getCSS(baseDir, cssFilename, cssCache)
if profileStyle:
# replace any https within the css with whatever prefix is needed
if httpPrefix != 'https':
profileStyle = \
profileStyle.replace('https://',
httpPrefix + '://')
iconsDir = getIconsDir(baseDir)
if nickname == 'news':
editor = False
moderator = False
else:
# is the user a moderator?
moderator = isModerator(baseDir, nickname)
# is the user a site editor?
editor = isEditor(baseDir, nickname)
showPublishButton = editor
htmlStr = htmlHeader(cssFilename, profileStyle)
bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain)
htmlStr += \
'<a href="/users/' + nickname + '/' + defaultTimeline + '">' + \
'<img loading="lazy" class="timeline-banner" ' + \
'src="/users/' + nickname + '/' + bannerFile + '" /></a>\n'
htmlStr += '<center>' + \
headerButtonsFrontScreen(translate, nickname,
'newswire', authorized,
iconsAsButtons, iconsDir) + '</center>'
htmlStr += \
getRightColumnContent(baseDir, nickname, domainFull,
httpPrefix, translate,
iconsDir, moderator, editor,
newswire, positiveVoting,
False, timelinePath, showPublishButton,
showPublishAsIcon, rssIconAtTop, False,
authorized, False)
htmlStr += htmlFooter()
return htmlStr
def htmlEditNewswire(cssCache: {}, translate: {}, baseDir: str, path: str,
domain: str, port: int, httpPrefix: str,
defaultTimeline: str) -> str:
"""Shows the edit newswire screen
"""
if '/users/' not in path:
return ''
path = path.replace('/inbox', '').replace('/outbox', '')
path = path.replace('/shares', '')
nickname = getNicknameFromActor(path)
if not nickname:
return ''
# is the user a moderator?
if not isModerator(baseDir, nickname):
return ''
cssFilename = baseDir + '/epicyon-links.css'
if os.path.isfile(baseDir + '/links.css'):
cssFilename = baseDir + '/links.css'
editCSS = getCSS(baseDir, cssFilename, cssCache)
if editCSS:
if httpPrefix != 'https':
editCSS = \
editCSS.replace('https://', httpPrefix + '://')
# filename of the banner shown at the top
bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain)
editNewswireForm = htmlHeader(cssFilename, editCSS)
# top banner
editNewswireForm += \
'<a href="/users/' + nickname + '/' + defaultTimeline + '" title="' + \
translate['Switch to timeline view'] + '" alt="' + \
translate['Switch to timeline view'] + '">\n'
editNewswireForm += '<img loading="lazy" class="timeline-banner" src="' + \
'/users/' + nickname + '/' + bannerFile + '" /></a>\n'
editNewswireForm += \
'<form enctype="multipart/form-data" method="POST" ' + \
'accept-charset="UTF-8" action="' + path + '/newswiredata">\n'
editNewswireForm += \
' <div class="vertical-center">\n'
editNewswireForm += \
' <p class="new-post-text">' + translate['Edit newswire'] + '</p>'
editNewswireForm += \
' <div class="container">\n'
# editNewswireForm += \
# ' <a href="' + pathOriginal + '"><button class="cancelbtn">' + \
# translate['Go Back'] + '</button></a>\n'
editNewswireForm += \
' <center>\n' + \
' <input type="submit" name="submitNewswire" value="' + \
translate['Submit'] + '">\n' + \
' </center>\n'
editNewswireForm += \
' </div>\n'
newswireFilename = baseDir + '/accounts/newswire.txt'
newswireStr = ''
if os.path.isfile(newswireFilename):
with open(newswireFilename, 'r') as fp:
newswireStr = fp.read()
editNewswireForm += \
'<div class="container">'
editNewswireForm += \
' ' + \
translate['Add RSS feed links below.'] + \
'<br>'
editNewswireForm += \
' <textarea id="message" name="editedNewswire" ' + \
'style="height:80vh">' + newswireStr + '</textarea>'
filterStr = ''
filterFilename = \
baseDir + '/accounts/news@' + domain + '/filters.txt'
if os.path.isfile(filterFilename):
with open(filterFilename, 'r') as filterfile:
filterStr = filterfile.read()
editNewswireForm += \
' <br><b><label class="labels">' + \
translate['Filtered words'] + '</label></b>\n'
editNewswireForm += ' <br><label class="labels">' + \
translate['One per line'] + '</label>'
editNewswireForm += ' <textarea id="message" ' + \
'name="filteredWordsNewswire" style="height:50vh">' + \
filterStr + '</textarea>\n'
hashtagRulesStr = ''
hashtagRulesFilename = \
baseDir + '/accounts/hashtagrules.txt'
if os.path.isfile(hashtagRulesFilename):
with open(hashtagRulesFilename, 'r') as rulesfile:
hashtagRulesStr = rulesfile.read()
editNewswireForm += \
' <br><b><label class="labels">' + \
translate['News tagging rules'] + '</label></b>\n'
editNewswireForm += ' <br><label class="labels">' + \
translate['One per line'] + '.</label>\n'
editNewswireForm += \
' <a href="' + \
'https://gitlab.com/bashrc2/epicyon/-/raw/main/hashtagrules.txt' + \
'">' + translate['See instructions'] + '</a>\n'
editNewswireForm += ' <textarea id="message" ' + \
'name="hashtagRulesList" style="height:80vh">' + \
hashtagRulesStr + '</textarea>\n'
editNewswireForm += \
'</div>'
editNewswireForm += htmlFooter()
return editNewswireForm

View File

@ -0,0 +1,251 @@
__filename__ = "webapp_person_options.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.1.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
import os
from shutil import copyfile
from petnames import getPetName
from person import isPersonSnoozed
from posts import isModerator
from utils import getDomainFromActor
from utils import getNicknameFromActor
from utils import getCSS
from blocking import isBlocked
from follow import isFollowingActor
from followingCalendar import receivingCalendarEvents
from webapp_utils import htmlHeader
from webapp_utils import htmlFooter
def htmlPersonOptions(cssCache: {}, translate: {}, baseDir: str,
domain: str, domainFull: str,
originPathStr: str,
optionsActor: str,
optionsProfileUrl: str,
optionsLink: str,
pageNumber: int,
donateUrl: str,
xmppAddress: str,
matrixAddress: str,
ssbAddress: str,
blogAddress: str,
toxAddress: str,
PGPpubKey: str,
PGPfingerprint: str,
emailAddress) -> str:
"""Show options for a person: view/follow/block/report
"""
optionsDomain, optionsPort = getDomainFromActor(optionsActor)
optionsDomainFull = optionsDomain
if optionsPort:
if optionsPort != 80 and optionsPort != 443:
optionsDomainFull = optionsDomain + ':' + str(optionsPort)
if os.path.isfile(baseDir + '/accounts/options-background-custom.jpg'):
if not os.path.isfile(baseDir + '/accounts/options-background.jpg'):
copyfile(baseDir + '/accounts/options-background.jpg',
baseDir + '/accounts/options-background.jpg')
followStr = 'Follow'
blockStr = 'Block'
nickname = None
optionsNickname = None
if originPathStr.startswith('/users/'):
nickname = originPathStr.split('/users/')[1]
if '/' in nickname:
nickname = nickname.split('/')[0]
if '?' in nickname:
nickname = nickname.split('?')[0]
followerDomain, followerPort = getDomainFromActor(optionsActor)
if isFollowingActor(baseDir, nickname, domain, optionsActor):
followStr = 'Unfollow'
optionsNickname = getNicknameFromActor(optionsActor)
optionsDomainFull = optionsDomain
if optionsPort:
if optionsPort != 80 and optionsPort != 443:
optionsDomainFull = optionsDomain + ':' + str(optionsPort)
if isBlocked(baseDir, nickname, domain,
optionsNickname, optionsDomainFull):
blockStr = 'Block'
optionsLinkStr = ''
if optionsLink:
optionsLinkStr = \
' <input type="hidden" name="postUrl" value="' + \
optionsLink + '">\n'
cssFilename = baseDir + '/epicyon-options.css'
if os.path.isfile(baseDir + '/options.css'):
cssFilename = baseDir + '/options.css'
profileStyle = getCSS(baseDir, cssFilename, cssCache)
if profileStyle:
profileStyle = \
profileStyle.replace('--follow-text-entry-width: 90%;',
'--follow-text-entry-width: 20%;')
if not os.path.isfile(baseDir + '/accounts/' +
'options-background.jpg'):
profileStyle = \
profileStyle.replace('background-image: ' +
'url("options-background.jpg");',
'background-image: none;')
# To snooze, or not to snooze? That is the question
snoozeButtonStr = 'Snooze'
if nickname:
if isPersonSnoozed(baseDir, nickname, domain, optionsActor):
snoozeButtonStr = 'Unsnooze'
donateStr = ''
if donateUrl:
donateStr = \
' <a href="' + donateUrl + \
'"><button class="button" name="submitDonate">' + \
translate['Donate'] + '</button></a>\n'
optionsStr = htmlHeader(cssFilename, profileStyle)
optionsStr += '<br><br>\n'
optionsStr += '<div class="options">\n'
optionsStr += ' <div class="optionsAvatar">\n'
optionsStr += ' <center>\n'
optionsStr += ' <a href="' + optionsActor + '">\n'
optionsStr += ' <img loading="lazy" src="' + optionsProfileUrl + \
'"/></a>\n'
handle = getNicknameFromActor(optionsActor) + '@' + optionsDomain
optionsStr += \
' <p class="optionsText">' + translate['Options for'] + \
' @' + handle + '</p>\n'
if emailAddress:
optionsStr += \
'<p class="imText">' + translate['Email'] + \
': <a href="mailto:' + \
emailAddress + '">' + emailAddress + '</a></p>\n'
if xmppAddress:
optionsStr += \
'<p class="imText">' + translate['XMPP'] + \
': <a href="xmpp:' + xmppAddress + '">' + \
xmppAddress + '</a></p>\n'
if matrixAddress:
optionsStr += \
'<p class="imText">' + translate['Matrix'] + ': ' + \
matrixAddress + '</p>\n'
if ssbAddress:
optionsStr += \
'<p class="imText">SSB: ' + ssbAddress + '</p>\n'
if blogAddress:
optionsStr += \
'<p class="imText">Blog: <a href="' + blogAddress + '">' + \
blogAddress + '</a></p>\n'
if toxAddress:
optionsStr += \
'<p class="imText">Tox: ' + toxAddress + '</p>\n'
if PGPfingerprint:
optionsStr += '<p class="pgp">PGP: ' + \
PGPfingerprint.replace('\n', '<br>') + '</p>\n'
if PGPpubKey:
optionsStr += '<p class="pgp">' + \
PGPpubKey.replace('\n', '<br>') + '</p>\n'
optionsStr += ' <form method="POST" action="' + \
originPathStr + '/personoptions">\n'
optionsStr += ' <input type="hidden" name="pageNumber" value="' + \
str(pageNumber) + '">\n'
optionsStr += ' <input type="hidden" name="actor" value="' + \
optionsActor + '">\n'
optionsStr += ' <input type="hidden" name="avatarUrl" value="' + \
optionsProfileUrl + '">\n'
if optionsNickname:
handle = optionsNickname + '@' + optionsDomainFull
petname = getPetName(baseDir, nickname, domain, handle)
optionsStr += \
' ' + translate['Petname'] + ': \n' + \
' <input type="text" name="optionpetname" value="' + \
petname + '">\n' \
' <button type="submit" class="buttonsmall" ' + \
'name="submitPetname">' + \
translate['Submit'] + '</button><br>\n'
# checkbox for receiving calendar events
if isFollowingActor(baseDir, nickname, domain, optionsActor):
checkboxStr = \
' <input type="checkbox" ' + \
'class="profilecheckbox" name="onCalendar" checked> ' + \
translate['Receive calendar events from this account'] + \
'\n <button type="submit" class="buttonsmall" ' + \
'name="submitOnCalendar">' + \
translate['Submit'] + '</button><br>\n'
if not receivingCalendarEvents(baseDir, nickname, domain,
optionsNickname, optionsDomainFull):
checkboxStr = checkboxStr.replace(' checked>', '>')
optionsStr += checkboxStr
# checkbox for permission to post to newswire
if optionsDomainFull == domainFull:
if isModerator(baseDir, nickname) and \
not isModerator(baseDir, optionsNickname):
newswireBlockedFilename = \
baseDir + '/accounts/' + \
optionsNickname + '@' + optionsDomain + '/.nonewswire'
checkboxStr = \
' <input type="checkbox" ' + \
'class="profilecheckbox" name="postsToNews" checked> ' + \
translate['Allow news posts'] + \
'\n <button type="submit" class="buttonsmall" ' + \
'name="submitPostToNews">' + \
translate['Submit'] + '</button><br>\n'
if os.path.isfile(newswireBlockedFilename):
checkboxStr = checkboxStr.replace(' checked>', '>')
optionsStr += checkboxStr
optionsStr += optionsLinkStr
optionsStr += \
' <a href="/"><button type="button" class="buttonIcon" ' + \
'name="submitBack">' + translate['Go Back'] + '</button></a>'
optionsStr += \
' <button type="submit" class="button" name="submitView">' + \
translate['View'] + '</button>'
optionsStr += donateStr
optionsStr += \
' <button type="submit" class="button" name="submit' + \
followStr + '">' + translate[followStr] + '</button>'
optionsStr += \
' <button type="submit" class="button" name="submit' + \
blockStr + '">' + translate[blockStr] + '</button>'
optionsStr += \
' <button type="submit" class="button" name="submitDM">' + \
translate['DM'] + '</button>'
optionsStr += \
' <button type="submit" class="button" name="submit' + \
snoozeButtonStr + '">' + translate[snoozeButtonStr] + '</button>'
optionsStr += \
' <button type="submit" class="button" name="submitReport">' + \
translate['Report'] + '</button>'
personNotes = ''
personNotesFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + \
'/notes/' + handle + '.txt'
if os.path.isfile(personNotesFilename):
with open(personNotesFilename, 'r') as fp:
personNotes = fp.read()
optionsStr += \
' <br><br>' + translate['Notes'] + ': \n'
optionsStr += ' <button type="submit" class="buttonsmall" ' + \
'name="submitPersonNotes">' + \
translate['Submit'] + '</button><br>\n'
optionsStr += \
' <textarea id="message" ' + \
'name="optionnotes" style="height:400px">' + \
personNotes + '</textarea>\n'
optionsStr += ' </form>\n'
optionsStr += '</center>\n'
optionsStr += '</div>\n'
optionsStr += '</div>\n'
optionsStr += htmlFooter()
return optionsStr

View File

@ -16,11 +16,16 @@ from cache import getPersonFromCache
from bookmarks import bookmarkedByPerson
from like import likedByPerson
from like import noOfLikes
from follow import isFollowingActor
from posts import isEditor
from posts import postIsMuted
from posts import getPersonBox
from posts import isDM
from posts import downloadAnnounce
from posts import populateRepliesJson
from utils import locatePost
from utils import loadJson
from utils import getCSS
from utils import getCachedPostDirectory
from utils import getCachedPostFilename
from utils import getProtocolPrefixes
@ -49,6 +54,9 @@ from webapp_utils import addEmojiToDisplayName
from webapp_utils import postContainsPublic
from webapp_utils import getContentWarningButton
from webapp_utils import getPostAttachmentsAsHtml
from webapp_utils import getIconsDir
from webapp_utils import htmlHeader
from webapp_utils import htmlFooter
from webapp_media import addEmbeddedElements
from webapp_question import insertQuestion
from devices import E2EEdecryptMessageFromDevice
@ -1215,3 +1223,162 @@ def individualPostAsHtml(allowDownloads: bool,
print('TIMING INDIV ' + boxName + ' 19 = ' + str(timeDiff))
return postHtml
def htmlIndividualPost(cssCache: {},
recentPostsCache: {}, maxRecentPosts: int,
translate: {},
baseDir: str, session, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, authorized: bool,
postJsonObject: {}, httpPrefix: str,
projectVersion: str, likedBy: str,
YTReplacementDomain: str,
showPublishedDateOnly: bool) -> str:
"""Show an individual post as html
"""
iconsDir = getIconsDir(baseDir)
postStr = ''
if likedBy:
likedByNickname = getNicknameFromActor(likedBy)
likedByDomain, likedByPort = getDomainFromActor(likedBy)
if likedByPort:
if likedByPort != 80 and likedByPort != 443:
likedByDomain += ':' + str(likedByPort)
likedByHandle = likedByNickname + '@' + likedByDomain
postStr += \
'<p>' + translate['Liked by'] + \
' <a href="' + likedBy + '">@' + \
likedByHandle + '</a>\n'
domainFull = domain
if port:
if port != 80 and port != 443:
domainFull = domain + ':' + str(port)
actor = '/users/' + nickname
followStr = ' <form method="POST" ' + \
'accept-charset="UTF-8" action="' + actor + '/searchhandle">\n'
followStr += \
' <input type="hidden" name="actor" value="' + actor + '">\n'
followStr += \
' <input type="hidden" name="searchtext" value="' + \
likedByHandle + '">\n'
if not isFollowingActor(baseDir, nickname, domainFull, likedBy):
followStr += ' <button type="submit" class="button" ' + \
'name="submitSearch">' + translate['Follow'] + '</button>\n'
followStr += ' <button type="submit" class="button" ' + \
'name="submitBack">' + translate['Go Back'] + '</button>\n'
followStr += ' </form>\n'
postStr += followStr + '</p>\n'
postStr += \
individualPostAsHtml(True, recentPostsCache, maxRecentPosts,
iconsDir, translate, None,
baseDir, session, wfRequest, personCache,
nickname, domain, port, postJsonObject,
None, True, False,
httpPrefix, projectVersion, 'inbox',
YTReplacementDomain,
showPublishedDateOnly,
False, authorized, False, False, False)
messageId = removeIdEnding(postJsonObject['id'])
# show the previous posts
if isinstance(postJsonObject['object'], dict):
while postJsonObject['object'].get('inReplyTo'):
postFilename = \
locatePost(baseDir, nickname, domain,
postJsonObject['object']['inReplyTo'])
if not postFilename:
break
postJsonObject = loadJson(postFilename)
if postJsonObject:
postStr = \
individualPostAsHtml(True, recentPostsCache,
maxRecentPosts,
iconsDir, translate, None,
baseDir, session, wfRequest,
personCache,
nickname, domain, port,
postJsonObject,
None, True, False,
httpPrefix, projectVersion, 'inbox',
YTReplacementDomain,
showPublishedDateOnly,
False, authorized,
False, False, False) + postStr
# show the following posts
postFilename = locatePost(baseDir, nickname, domain, messageId)
if postFilename:
# is there a replies file for this post?
repliesFilename = postFilename.replace('.json', '.replies')
if os.path.isfile(repliesFilename):
# get items from the replies file
repliesJson = {
'orderedItems': []
}
populateRepliesJson(baseDir, nickname, domain,
repliesFilename, authorized, repliesJson)
# add items to the html output
for item in repliesJson['orderedItems']:
postStr += \
individualPostAsHtml(True, recentPostsCache,
maxRecentPosts,
iconsDir, translate, None,
baseDir, session, wfRequest,
personCache,
nickname, domain, port, item,
None, True, False,
httpPrefix, projectVersion, 'inbox',
YTReplacementDomain,
showPublishedDateOnly,
False, authorized,
False, False, False)
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
postsCSS = getCSS(baseDir, cssFilename, cssCache)
if postsCSS:
if httpPrefix != 'https':
postsCSS = postsCSS.replace('https://',
httpPrefix + '://')
return htmlHeader(cssFilename, postsCSS) + postStr + htmlFooter()
def htmlPostReplies(cssCache: {},
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, baseDir: str,
session, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, repliesJson: {},
httpPrefix: str, projectVersion: str,
YTReplacementDomain: str,
showPublishedDateOnly: bool) -> str:
"""Show the replies to an individual post as html
"""
iconsDir = getIconsDir(baseDir)
repliesStr = ''
if repliesJson.get('orderedItems'):
for item in repliesJson['orderedItems']:
repliesStr += \
individualPostAsHtml(True, recentPostsCache,
maxRecentPosts,
iconsDir, translate, None,
baseDir, session, wfRequest, personCache,
nickname, domain, port, item,
None, True, False,
httpPrefix, projectVersion, 'inbox',
YTReplacementDomain,
showPublishedDateOnly,
False, False, False, False, False)
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
postsCSS = getCSS(baseDir, cssFilename, cssCache)
if postsCSS:
if httpPrefix != 'https':
postsCSS = postsCSS.replace('https://',
httpPrefix + '://')
return htmlHeader(cssFilename, postsCSS) + repliesStr + htmlFooter()

1472
webapp_profile.py 100644

File diff suppressed because it is too large Load Diff

1411
webapp_timeline.py 100644

File diff suppressed because it is too large Load Diff

View File

@ -740,3 +740,73 @@ def htmlPostSeparator(baseDir: str, column: str) -> str:
'<img src="/' + iconsDir + '/' + filename + '"/>' + \
'</center></div>\n'
return separatorStr
def headerButtonsFrontScreen(translate: {},
nickname: str, boxName: str,
authorized: bool,
iconsAsButtons: bool,
iconsDir: bool) -> str:
"""Returns the header buttons for the front page of a news instance
"""
headerStr = ''
if nickname == 'news':
buttonFeatures = 'buttonMobile'
buttonNewswire = 'buttonMobile'
buttonLinks = 'buttonMobile'
if boxName == 'features':
buttonFeatures = 'buttonselected'
elif boxName == 'newswire':
buttonNewswire = 'buttonselected'
elif boxName == 'links':
buttonLinks = 'buttonselected'
headerStr += \
' <a href="/">' + \
'<button class="' + buttonFeatures + '">' + \
'<span>' + translate['Features'] + \
'</span></button></a>'
if not authorized:
headerStr += \
' <a href="/login">' + \
'<button class="buttonMobile">' + \
'<span>' + translate['Login'] + \
'</span></button></a>'
if iconsAsButtons:
headerStr += \
' <a href="/users/news/newswiremobile">' + \
'<button class="' + buttonNewswire + '">' + \
'<span>' + translate['Newswire'] + \
'</span></button></a>'
headerStr += \
' <a href="/users/news/linksmobile">' + \
'<button class="' + buttonLinks + '">' + \
'<span>' + translate['Links'] + \
'</span></button></a>'
else:
headerStr += \
' <a href="' + \
'/users/news/newswiremobile">' + \
'<img loading="lazy" src="/' + iconsDir + \
'/newswire.png" title="' + translate['Newswire'] + \
'" alt="| ' + translate['Newswire'] + '"/></a>\n'
headerStr += \
' <a href="' + \
'/users/news/linksmobile">' + \
'<img loading="lazy" src="/' + iconsDir + \
'/links.png" title="' + translate['Links'] + \
'" alt="| ' + translate['Links'] + '"/></a>\n'
else:
if not authorized:
headerStr += \
' <a href="/login">' + \
'<button class="buttonMobile">' + \
'<span>' + translate['Login'] + \
'</span></button></a>'
if headerStr:
headerStr = \
'\n <div class="frontPageMobileButtons">\n' + \
headerStr + \
' </div>\n'
return headerStr