diff --git a/README.md b/README.md index 9d530e8f7..548807b5b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Add issues on https://gitlab.com/bashrc2/epicyon/-/issues
Epicyon, meaning "more than a dog". Largest of the Borophaginae which lived in North America 20-5 million years ago.
- + diff --git a/daemon.py b/daemon.py index 77642227b..f3ffefb95 100644 --- a/daemon.py +++ b/daemon.py @@ -117,9 +117,9 @@ from webapp_utils import setBlogAddress from webapp_utils import getBlogAddress from webapp_calendar import htmlCalendarDeleteConfirm from webapp_calendar import htmlCalendar +from webapp_about import htmlAbout from webapp import htmlFollowingList from webapp import htmlDeletePost -from webapp import htmlAbout from webapp import htmlRemoveSharedItem from webapp import htmlUnblockConfirm from webapp_person_options import htmlPersonOptions @@ -133,16 +133,15 @@ 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_moderation import htmlModeration +from webapp_moderation import htmlModerationInfo from webapp_create_post import htmlNewPost -from webapp import htmlLogin -from webapp import htmlSuspended -from webapp import htmlGetLoginCredentials +from webapp_login import htmlLogin +from webapp_login import htmlGetLoginCredentials +from webapp_suspended import htmlSuspended +from webapp_tos import htmlTermsOfService from webapp import htmlFollowConfirm from webapp import htmlUnfollowConfirm -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 @@ -154,6 +153,7 @@ 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_column_right import htmlEditNewsPost from webapp_search import htmlSkillsSearch from webapp_search import htmlHistorySearch from webapp_search import htmlHashtagSearch @@ -166,6 +166,7 @@ from shares import getSharesFeedForPerson from shares import addShare from shares import removeShare from shares import expireShares +from utils import getCSS from utils import firstParagraphFromString from utils import clearFromPostCaches from utils import containsInvalidChars @@ -191,6 +192,7 @@ from utils import isSuspended from manualapprove import manualDenyFollowRequest from manualapprove import manualApproveFollowRequest from announce import createAnnounce +from content import dangerousMarkup from content import replaceEmojiFromTags from content import addHtmlTags from content import extractMediaInFormPOST @@ -2891,10 +2893,13 @@ class PubServer(BaseHTTPRequestHandler): return linksFilename = baseDir + '/accounts/links.txt' + aboutFilename = baseDir + '/accounts/about.txt' + TOSFilename = baseDir + '/accounts/tos.txt' # extract all of the text fields into a dict fields = \ extractTextFieldsInPOST(postBytes, boundary, debug) + if fields.get('editedLinks'): linksStr = fields['editedLinks'] linksFile = open(linksFilename, "w+") @@ -2905,6 +2910,31 @@ class PubServer(BaseHTTPRequestHandler): if os.path.isfile(linksFilename): os.remove(linksFilename) + adminNickname = \ + getConfigParam(baseDir, 'admin') + if nickname == adminNickname: + if fields.get('editedAbout'): + aboutStr = fields['editedAbout'] + if not dangerousMarkup(aboutStr): + aboutFile = open(aboutFilename, "w+") + if aboutFile: + aboutFile.write(aboutStr) + aboutFile.close() + else: + if os.path.isfile(aboutFilename): + os.remove(aboutFilename) + + if fields.get('editedTOS'): + TOSStr = fields['editedTOS'] + if not dangerousMarkup(TOSStr): + TOSFile = open(TOSFilename, "w+") + if TOSFile: + TOSFile.write(TOSStr) + TOSFile.close() + else: + if os.path.isfile(TOSFilename): + os.remove(TOSFilename) + # redirect back to the default timeline if callingDomain.endswith('.onion') and \ onionDomain: @@ -4678,7 +4708,8 @@ class PubServer(BaseHTTPRequestHandler): emailAddress = getEmailAddress(actorJson) PGPpubKey = getPGPpubKey(actorJson) PGPfingerprint = getPGPfingerprint(actorJson) - msg = htmlPersonOptions(self.server.cssCache, + msg = htmlPersonOptions(self.server.defaultTimeline, + self.server.cssCache, self.server.translate, baseDir, domain, domainFull, @@ -8250,8 +8281,9 @@ class PubServer(BaseHTTPRequestHandler): tries = 0 while tries < 5: try: - with open(path, 'r') as cssfile: - css = cssfile.read() + css = getCSS(self.server.baseDir, path, + self.server.cssCache) + if css: break except Exception as e: print(e) diff --git a/default_tos.txt b/default_tos.txt index 1004961e6..176240893 100644 --- a/default_tos.txt +++ b/default_tos.txt @@ -26,7 +26,7 @@

This system will not federate with instances whose moderation policy is incompatible with the content policy described above. If an instance lacks a moderation policy, or refuses to enforce one, it will be assumed to be incompatible.

-

Use of Data for Research Purposes

+

Use of User Generated Content for Research

Data may not be "scraped" or otherwise obtained from this instance and used for academic research or cited within research publications without the prior written permission of the administrator. Financial remedy will be sought through the courts from any researcher publishing data obtained from this instance without consent.

diff --git a/epicyon-options.css b/epicyon-options.css index 7886378e6..9e27a2bc1 100644 --- a/epicyon-options.css +++ b/epicyon-options.css @@ -33,6 +33,7 @@ --follow-text-size2: 40px; --follow-text-entry-width: 90%; --focus-color: white; + --petname-width-chars: 16ch; } @font-face { @@ -139,6 +140,7 @@ a:focus { input[type=text] { width: var(--follow-text-entry-width); clear: both; + min-width: var(--petname-width-chars); font-size: 24px; text-align: center; color: var(--text-entry-foreground); @@ -216,6 +218,7 @@ a:focus { clear: both; font-size: 40px; text-align: center; + min-width: var(--petname-width-chars); color: var(--text-entry-foreground); background-color: var(--text-entry-background); font-family: Arial, Helvetica, sans-serif; diff --git a/img/screenshot_starlight.jpg b/img/screenshot_starlight.jpg new file mode 100644 index 000000000..516662b2c Binary files /dev/null and b/img/screenshot_starlight.jpg differ diff --git a/webapp.py b/webapp.py index f51ff06ff..02061882b 100644 --- a/webapp.py +++ b/webapp.py @@ -6,17 +6,13 @@ __maintainer__ = "Bob Mottram" __email__ = "bob@freedombone.net" __status__ = "Production" -import time import os from shutil import copyfile from utils import getCSS from utils import getNicknameFromActor from utils import getDomainFromActor from utils import locatePost -from utils import noOfAccounts from utils import loadJson -from utils import getConfigParam -from posts import isEditor from shares import getValidSharedItemID from webapp_utils import getAltPath from webapp_utils import getIconsDir @@ -51,395 +47,6 @@ def htmlFollowingList(cssCache: {}, baseDir: str, return '' -def htmlModerationInfo(cssCache: {}, translate: {}, - baseDir: str, httpPrefix: str) -> str: - msgStr1 = \ - 'These are globally blocked for all accounts on this instance' - msgStr2 = \ - 'Any blocks or suspensions made by moderators will be shown here.' - infoForm = '' - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - infoCSS = getCSS(baseDir, cssFilename, cssCache) - if infoCSS: - if httpPrefix != 'https': - infoCSS = infoCSS.replace('https://', - httpPrefix + '://') - infoForm = htmlHeader(cssFilename, infoCSS) - - infoForm += \ - '

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

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

' + \ - translate[msgStr2] + \ - '

' - infoForm += htmlFooter() - return infoForm - - -def htmlEditNewsPost(cssCache: {}, translate: {}, baseDir: str, path: str, - domain: str, port: int, - httpPrefix: str, postUrl: str) -> str: - """Edits a news post - """ - if '/users/' not in path: - return '' - pathOriginal = path - - nickname = getNicknameFromActor(path) - if not nickname: - return '' - - # is the user an editor? - if not isEditor(baseDir, nickname): - return '' - - postUrl = postUrl.replace('/', '#') - postFilename = locatePost(baseDir, nickname, domain, postUrl) - if not postFilename: - return '' - postJsonObject = loadJson(postFilename) - if not postJsonObject: - return '' - - cssFilename = baseDir + '/epicyon-links.css' - if os.path.isfile(baseDir + '/links.css'): - cssFilename = baseDir + '/links.css' - - editCSS = getCSS(baseDir, cssFilename, cssCache) - if editCSS: - if httpPrefix != 'https': - editCSS = \ - editCSS.replace('https://', httpPrefix + '://') - - editNewsPostForm = htmlHeader(cssFilename, editCSS) - editNewsPostForm += \ - '
\n' - editNewsPostForm += \ - '
\n' - editNewsPostForm += \ - '

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

' - editNewsPostForm += \ - '
\n' - editNewsPostForm += \ - ' ' + \ - '\n' - editNewsPostForm += \ - ' \n' - editNewsPostForm += \ - '
\n' - - editNewsPostForm += \ - '
' - - editNewsPostForm += \ - ' \n' - - newsPostTitle = postJsonObject['object']['summary'] - editNewsPostForm += \ - '
\n' - - newsPostContent = postJsonObject['object']['content'] - editNewsPostForm += \ - ' ' - - editNewsPostForm += \ - '
' - - editNewsPostForm += htmlFooter() - return editNewsPostForm - - -def htmlGetLoginCredentials(loginParams: str, - lastLoginTime: int) -> (str, str, bool): - """Receives login credentials via HTTPServer POST - """ - if not loginParams.startswith('username='): - return None, None, None - # minimum time between login attempts - currTime = int(time.time()) - if currTime < lastLoginTime+10: - return None, None, None - if '&' not in loginParams: - return None, None, None - loginArgs = loginParams.split('&') - nickname = None - password = None - register = False - for arg in loginArgs: - if '=' in arg: - if arg.split('=', 1)[0] == 'username': - nickname = arg.split('=', 1)[1] - elif arg.split('=', 1)[0] == 'password': - password = arg.split('=', 1)[1] - elif arg.split('=', 1)[0] == 'register': - register = True - return nickname, password, register - - -def htmlLogin(cssCache: {}, translate: {}, - baseDir: str, autocomplete=True) -> str: - """Shows the login screen - """ - accounts = noOfAccounts(baseDir) - - loginImage = 'login.png' - loginImageFilename = None - if os.path.isfile(baseDir + '/accounts/' + loginImage): - loginImageFilename = baseDir + '/accounts/' + loginImage - elif os.path.isfile(baseDir + '/accounts/login.jpg'): - loginImage = 'login.jpg' - loginImageFilename = baseDir + '/accounts/' + loginImage - elif os.path.isfile(baseDir + '/accounts/login.jpeg'): - loginImage = 'login.jpeg' - loginImageFilename = baseDir + '/accounts/' + loginImage - elif os.path.isfile(baseDir + '/accounts/login.gif'): - loginImage = 'login.gif' - loginImageFilename = baseDir + '/accounts/' + loginImage - elif os.path.isfile(baseDir + '/accounts/login.webp'): - loginImage = 'login.webp' - loginImageFilename = baseDir + '/accounts/' + loginImage - elif os.path.isfile(baseDir + '/accounts/login.avif'): - loginImage = 'login.avif' - loginImageFilename = baseDir + '/accounts/' + loginImage - - if not loginImageFilename: - loginImageFilename = baseDir + '/accounts/' + loginImage - copyfile(baseDir + '/img/login.png', loginImageFilename) - - if os.path.isfile(baseDir + '/accounts/login-background-custom.jpg'): - if not os.path.isfile(baseDir + '/accounts/login-background.jpg'): - copyfile(baseDir + '/accounts/login-background-custom.jpg', - baseDir + '/accounts/login-background.jpg') - - if accounts > 0: - loginText = \ - '' - else: - loginText = \ - '' - loginText += \ - '' - if os.path.isfile(baseDir + '/accounts/login.txt'): - # custom login message - with open(baseDir + '/accounts/login.txt', 'r') as file: - loginText = '' - - cssFilename = baseDir + '/epicyon-login.css' - if os.path.isfile(baseDir + '/login.css'): - cssFilename = baseDir + '/login.css' - - loginCSS = getCSS(baseDir, cssFilename, cssCache) - if not loginCSS: - print('ERROR: login css file missing ' + cssFilename) - return None - - # show the register button - registerButtonStr = '' - if getConfigParam(baseDir, 'registration') == 'open': - if int(getConfigParam(baseDir, 'registrationsRemaining')) > 0: - if accounts > 0: - idx = 'Welcome. Please login or register a new account.' - loginText = \ - '' - registerButtonStr = \ - '' - - TOSstr = \ - '' - TOSstr += \ - '' - - loginButtonStr = '' - if accounts > 0: - loginButtonStr = \ - '' - - autocompleteStr = '' - if not autocomplete: - autocompleteStr = 'autocomplete="off" value=""' - - loginForm = htmlHeader(cssFilename, loginCSS) - loginForm += '
\n' - loginForm += '\n' - loginForm += '
\n' - loginForm += \ - ' login image\n' - loginForm += loginText + TOSstr + '\n' - loginForm += '
\n' - loginForm += '\n' - loginForm += '
\n' - loginForm += ' \n' - loginForm += \ - ' \n' - loginForm += '\n' - loginForm += ' \n' - loginForm += \ - ' \n' - loginForm += loginButtonStr + registerButtonStr + '\n' - loginForm += '
\n' - loginForm += '\n' - loginForm += \ - '' + \ - '' + \
-        translate['Get the source code'] + '\n' - loginForm += htmlFooter() - return loginForm - - -def htmlTermsOfService(cssCache: {}, baseDir: str, - httpPrefix: str, domainFull: str) -> str: - """Show the terms of service screen - """ - adminNickname = getConfigParam(baseDir, 'admin') - if not os.path.isfile(baseDir + '/accounts/tos.txt'): - copyfile(baseDir + '/default_tos.txt', - baseDir + '/accounts/tos.txt') - - if os.path.isfile(baseDir + '/accounts/login-background-custom.jpg'): - if not os.path.isfile(baseDir + '/accounts/login-background.jpg'): - copyfile(baseDir + '/accounts/login-background-custom.jpg', - baseDir + '/accounts/login-background.jpg') - - TOSText = 'Terms of Service go here.' - if os.path.isfile(baseDir + '/accounts/tos.txt'): - with open(baseDir + '/accounts/tos.txt', 'r') as file: - TOSText = file.read() - - TOSForm = '' - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - termsCSS = getCSS(baseDir, cssFilename, cssCache) - if termsCSS: - if httpPrefix != 'https': - termsCSS = termsCSS.replace('https://', httpPrefix+'://') - - TOSForm = htmlHeader(cssFilename, termsCSS) - TOSForm += '
' + TOSText + '
\n' - if adminNickname: - adminActor = httpPrefix + '://' + domainFull + \ - '/users/' + adminNickname - TOSForm += \ - '
\n' + \ - '

Administered by ' + adminNickname + '

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

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

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

Administered by ' + adminNickname + '

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

Account Suspended

\n' - suspendedForm += '

See Terms of Service

\n' - suspendedForm += '
\n' - suspendedForm += htmlFooter() - return suspendedForm - - def htmlRemoveSharedItem(cssCache: {}, translate: {}, baseDir: str, actor: str, shareName: str, callingDomain: str) -> str: diff --git a/webapp_about.py b/webapp_about.py new file mode 100644 index 000000000..72b522398 --- /dev/null +++ b/webapp_about.py @@ -0,0 +1,62 @@ +__filename__ = "webapp_about.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 getCSS +from utils import getConfigParam +from webapp_utils import htmlHeader +from webapp_utils import htmlFooter + + +def htmlAbout(cssCache: {}, baseDir: str, httpPrefix: str, + domainFull: str, onionDomain: str) -> str: + """Show the about screen + """ + adminNickname = getConfigParam(baseDir, 'admin') + if not os.path.isfile(baseDir + '/accounts/about.txt'): + copyfile(baseDir + '/default_about.txt', + baseDir + '/accounts/about.txt') + + if os.path.isfile(baseDir + '/accounts/login-background-custom.jpg'): + if not os.path.isfile(baseDir + '/accounts/login-background.jpg'): + copyfile(baseDir + '/accounts/login-background-custom.jpg', + baseDir + '/accounts/login-background.jpg') + + aboutText = 'Information about this instance goes here.' + if os.path.isfile(baseDir + '/accounts/about.txt'): + with open(baseDir + '/accounts/about.txt', 'r') as aboutFile: + aboutText = aboutFile.read() + + aboutForm = '' + cssFilename = baseDir + '/epicyon-profile.css' + if os.path.isfile(baseDir + '/epicyon.css'): + cssFilename = baseDir + '/epicyon.css' + + aboutCSS = getCSS(baseDir, cssFilename, cssCache) + if aboutCSS: + if httpPrefix != 'http': + aboutCSS = aboutCSS.replace('https://', + httpPrefix + '://') + + aboutForm = htmlHeader(cssFilename, aboutCSS) + aboutForm += '
' + aboutText + '
' + if onionDomain: + aboutForm += \ + '
\n' + \ + '

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

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

Administered by ' + adminNickname + '

\n' + \ + '
\n' + aboutForm += htmlFooter() + return aboutForm diff --git a/webapp_column_left.py b/webapp_column_left.py index 736bfb1bb..9a7216ebd 100644 --- a/webapp_column_left.py +++ b/webapp_column_left.py @@ -17,7 +17,7 @@ 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 htmlHeaderWithExternalStyle from webapp_utils import htmlFooter from webapp_utils import getBannerFile @@ -125,6 +125,13 @@ def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str, # if showHeaderImage: # htmlStr += '
' + htmlStr += \ + '' + htmlStr += \ + '' + linksFilename = baseDir + '/accounts/links.txt' linksFileContainsEntries = False if os.path.isfile(linksFilename): @@ -213,7 +220,7 @@ def htmlLinksMobile(cssCache: {}, baseDir: str, if ':' in domain: domain = domain.split(':')[0] - htmlStr = htmlHeader(cssFilename, profileStyle) + htmlStr = htmlHeaderWithExternalStyle(cssFilename, profileStyle) bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) htmlStr += \ '' + \ @@ -265,7 +272,7 @@ def htmlEditLinks(cssCache: {}, translate: {}, baseDir: str, path: str, # filename of the banner shown at the top bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) - editLinksForm = htmlHeader(cssFilename, editCSS) + editLinksForm = htmlHeaderWithExternalStyle(cssFilename, editCSS) # top banner editLinksForm += \ @@ -284,9 +291,6 @@ def htmlEditLinks(cssCache: {}, translate: {}, baseDir: str, path: str, '

' + translate['Edit Links'] + '

' editLinksForm += \ '
\n' - # editLinksForm += \ - # ' \n' editLinksForm += \ '
\n' + \ ' ' + editLinksForm += \ + ' ' + \ + translate['About this Instance'] + \ + '
' + editLinksForm += \ + ' ' + editLinksForm += \ + '
' + + TOSFilename = baseDir + '/accounts/tos.txt' + TOSStr = '' + if os.path.isfile(TOSFilename): + with open(TOSFilename, 'r') as fp: + TOSStr = fp.read() + + editLinksForm += \ + '
' + editLinksForm += \ + ' ' + \ + translate['Terms of Service'] + \ + '
' + editLinksForm += \ + ' ' + editLinksForm += \ + '
' + editLinksForm += htmlFooter() return editLinksForm diff --git a/webapp_column_right.py b/webapp_column_right.py index c8b72a7b4..46529a8a7 100644 --- a/webapp_column_right.py +++ b/webapp_column_right.py @@ -10,6 +10,8 @@ import os from datetime import datetime from shutil import copyfile from content import removeLongWords +from utils import locatePost +from utils import loadJson from utils import getCSS from utils import getConfigParam from utils import votesOnNewswireItem @@ -18,7 +20,7 @@ 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 htmlHeaderWithExternalStyle from webapp_utils import htmlFooter from webapp_utils import getBannerFile from webapp_utils import htmlPostSeparator @@ -312,7 +314,7 @@ def htmlCitations(baseDir: str, nickname: str, domain: str, # iconsDir = getIconsDir(baseDir) - htmlStr = htmlHeader(cssFilename, profileStyle) + htmlStr = htmlHeaderWithExternalStyle(cssFilename, profileStyle) # top banner bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) @@ -422,7 +424,7 @@ def htmlNewswireMobile(cssCache: {}, baseDir: str, nickname: str, showPublishButton = editor - htmlStr = htmlHeader(cssFilename, profileStyle) + htmlStr = htmlHeaderWithExternalStyle(cssFilename, profileStyle) bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) htmlStr += \ @@ -477,7 +479,7 @@ def htmlEditNewswire(cssCache: {}, translate: {}, baseDir: str, path: str, # filename of the banner shown at the top bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) - editNewswireForm = htmlHeader(cssFilename, editCSS) + editNewswireForm = htmlHeaderWithExternalStyle(cssFilename, editCSS) # top banner editNewswireForm += \ @@ -565,3 +567,81 @@ def htmlEditNewswire(cssCache: {}, translate: {}, baseDir: str, path: str, editNewswireForm += htmlFooter() return editNewswireForm + + +def htmlEditNewsPost(cssCache: {}, translate: {}, baseDir: str, path: str, + domain: str, port: int, + httpPrefix: str, postUrl: str) -> str: + """Edits a news post on the news/features timeline + """ + if '/users/' not in path: + return '' + pathOriginal = path + + nickname = getNicknameFromActor(path) + if not nickname: + return '' + + # is the user an editor? + if not isEditor(baseDir, nickname): + return '' + + postUrl = postUrl.replace('/', '#') + postFilename = locatePost(baseDir, nickname, domain, postUrl) + if not postFilename: + return '' + postJsonObject = loadJson(postFilename) + if not postJsonObject: + return '' + + cssFilename = baseDir + '/epicyon-links.css' + if os.path.isfile(baseDir + '/links.css'): + cssFilename = baseDir + '/links.css' + + editCSS = getCSS(baseDir, cssFilename, cssCache) + if editCSS: + if httpPrefix != 'https': + editCSS = \ + editCSS.replace('https://', httpPrefix + '://') + + editNewsPostForm = htmlHeaderWithExternalStyle(cssFilename, editCSS) + editNewsPostForm += \ + '
\n' + editNewsPostForm += \ + '
\n' + editNewsPostForm += \ + '

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

' + editNewsPostForm += \ + '
\n' + editNewsPostForm += \ + ' ' + \ + '\n' + editNewsPostForm += \ + ' \n' + editNewsPostForm += \ + '
\n' + + editNewsPostForm += \ + '
' + + editNewsPostForm += \ + ' \n' + + newsPostTitle = postJsonObject['object']['summary'] + editNewsPostForm += \ + '
\n' + + newsPostContent = postJsonObject['object']['content'] + editNewsPostForm += \ + ' ' + + editNewsPostForm += \ + '
' + + editNewsPostForm += htmlFooter() + return editNewsPostForm diff --git a/webapp_create_post.py b/webapp_create_post.py index b69edc7ee..49f7e2cf8 100644 --- a/webapp_create_post.py +++ b/webapp_create_post.py @@ -13,7 +13,7 @@ from utils import getNicknameFromActor from utils import getDomainFromActor from webapp_utils import getIconsDir from webapp_utils import getBannerFile -from webapp_utils import htmlHeader +from webapp_utils import htmlHeaderWithExternalStyle from webapp_utils import htmlFooter @@ -558,7 +558,7 @@ def htmlNewPost(cssCache: {}, mediaInstance: bool, translate: {}, dateAndLocation += '\n' dateAndLocation += '
\n' - newPostForm = htmlHeader(cssFilename, newPostCSS) + newPostForm = htmlHeaderWithExternalStyle(cssFilename, newPostCSS) newPostForm += \ ' (str, str, bool): + """Receives login credentials via HTTPServer POST + """ + if not loginParams.startswith('username='): + return None, None, None + # minimum time between login attempts + currTime = int(time.time()) + if currTime < lastLoginTime+10: + return None, None, None + if '&' not in loginParams: + return None, None, None + loginArgs = loginParams.split('&') + nickname = None + password = None + register = False + for arg in loginArgs: + if '=' in arg: + if arg.split('=', 1)[0] == 'username': + nickname = arg.split('=', 1)[1] + elif arg.split('=', 1)[0] == 'password': + password = arg.split('=', 1)[1] + elif arg.split('=', 1)[0] == 'register': + register = True + return nickname, password, register + + +def htmlLogin(cssCache: {}, translate: {}, + baseDir: str, autocomplete=True) -> str: + """Shows the login screen + """ + accounts = noOfAccounts(baseDir) + + loginImage = 'login.png' + loginImageFilename = None + if os.path.isfile(baseDir + '/accounts/' + loginImage): + loginImageFilename = baseDir + '/accounts/' + loginImage + elif os.path.isfile(baseDir + '/accounts/login.jpg'): + loginImage = 'login.jpg' + loginImageFilename = baseDir + '/accounts/' + loginImage + elif os.path.isfile(baseDir + '/accounts/login.jpeg'): + loginImage = 'login.jpeg' + loginImageFilename = baseDir + '/accounts/' + loginImage + elif os.path.isfile(baseDir + '/accounts/login.gif'): + loginImage = 'login.gif' + loginImageFilename = baseDir + '/accounts/' + loginImage + elif os.path.isfile(baseDir + '/accounts/login.webp'): + loginImage = 'login.webp' + loginImageFilename = baseDir + '/accounts/' + loginImage + elif os.path.isfile(baseDir + '/accounts/login.avif'): + loginImage = 'login.avif' + loginImageFilename = baseDir + '/accounts/' + loginImage + + if not loginImageFilename: + loginImageFilename = baseDir + '/accounts/' + loginImage + copyfile(baseDir + '/img/login.png', loginImageFilename) + + if os.path.isfile(baseDir + '/accounts/login-background-custom.jpg'): + if not os.path.isfile(baseDir + '/accounts/login-background.jpg'): + copyfile(baseDir + '/accounts/login-background-custom.jpg', + baseDir + '/accounts/login-background.jpg') + + if accounts > 0: + loginText = \ + '' + else: + loginText = \ + '' + loginText += \ + '' + if os.path.isfile(baseDir + '/accounts/login.txt'): + # custom login message + with open(baseDir + '/accounts/login.txt', 'r') as file: + loginText = '' + + cssFilename = baseDir + '/epicyon-login.css' + if os.path.isfile(baseDir + '/login.css'): + cssFilename = baseDir + '/login.css' + + loginCSS = getCSS(baseDir, cssFilename, cssCache) + if not loginCSS: + print('ERROR: login css file missing ' + cssFilename) + return None + + # show the register button + registerButtonStr = '' + if getConfigParam(baseDir, 'registration') == 'open': + if int(getConfigParam(baseDir, 'registrationsRemaining')) > 0: + if accounts > 0: + idx = 'Welcome. Please login or register a new account.' + loginText = \ + '' + registerButtonStr = \ + '' + + TOSstr = \ + '' + TOSstr += \ + '' + + loginButtonStr = '' + if accounts > 0: + loginButtonStr = \ + '' + + autocompleteStr = '' + if not autocomplete: + autocompleteStr = 'autocomplete="off" value=""' + + loginForm = htmlHeader(cssFilename, loginCSS) + loginForm += '
\n' + loginForm += '\n' + loginForm += '
\n' + loginForm += \ + ' login image\n' + loginForm += loginText + TOSstr + '\n' + loginForm += '
\n' + loginForm += '\n' + loginForm += '
\n' + loginForm += ' \n' + loginForm += \ + ' \n' + loginForm += '\n' + loginForm += ' \n' + loginForm += \ + ' \n' + loginForm += loginButtonStr + registerButtonStr + '\n' + loginForm += '
\n' + loginForm += '
\n' + loginForm += \ + '' + \ + '' + \
+        translate['Get the source code'] + '\n' + loginForm += htmlFooter() + return loginForm diff --git a/webapp_moderation.py b/webapp_moderation.py new file mode 100644 index 000000000..a3ec54292 --- /dev/null +++ b/webapp_moderation.py @@ -0,0 +1,111 @@ +__filename__ = "webapp_moderation.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.1.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + +import os +from utils import getCSS +from webapp_timeline import htmlTimeline +from webapp_utils import htmlHeader +from webapp_utils import htmlFooter + + +def htmlModeration(cssCache: {}, defaultTimeline: str, + recentPostsCache: {}, maxRecentPosts: int, + translate: {}, pageNumber: int, itemsPerPage: int, + session, baseDir: str, wfRequest: {}, personCache: {}, + nickname: str, domain: str, port: int, inboxJson: {}, + allowDeletion: bool, + httpPrefix: str, projectVersion: str, + YTReplacementDomain: str, + showPublishedDateOnly: bool, + newswire: {}, positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: + """Show the moderation feed as html + This is what you see when selecting the "mod" timeline + """ + return htmlTimeline(cssCache, defaultTimeline, + recentPostsCache, maxRecentPosts, + translate, pageNumber, + itemsPerPage, session, baseDir, wfRequest, personCache, + nickname, domain, port, inboxJson, 'moderation', + allowDeletion, httpPrefix, projectVersion, True, False, + YTReplacementDomain, showPublishedDateOnly, + newswire, False, False, positiveVoting, + showPublishAsIcon, fullWidthTimelineButtonHeader, + iconsAsButtons, rssIconAtTop, publishButtonAtTop, + authorized) + + +def htmlModerationInfo(cssCache: {}, translate: {}, + baseDir: str, httpPrefix: str) -> str: + msgStr1 = \ + 'These are globally blocked for all accounts on this instance' + msgStr2 = \ + 'Any blocks or suspensions made by moderators will be shown here.' + infoForm = '' + cssFilename = baseDir + '/epicyon-profile.css' + if os.path.isfile(baseDir + '/epicyon.css'): + cssFilename = baseDir + '/epicyon.css' + + infoCSS = getCSS(baseDir, cssFilename, cssCache) + if infoCSS: + if httpPrefix != 'https': + infoCSS = infoCSS.replace('https://', + httpPrefix + '://') + infoForm = htmlHeader(cssFilename, infoCSS) + + infoForm += \ + '

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

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

' + \ + translate[msgStr2] + \ + '

' + infoForm += htmlFooter() + return infoForm diff --git a/webapp_person_options.py b/webapp_person_options.py index 9c37e00de..c6cb8df0e 100644 --- a/webapp_person_options.py +++ b/webapp_person_options.py @@ -21,7 +21,8 @@ from webapp_utils import htmlHeader from webapp_utils import htmlFooter -def htmlPersonOptions(cssCache: {}, translate: {}, baseDir: str, +def htmlPersonOptions(defaultTimeline: str, + cssCache: {}, translate: {}, baseDir: str, domain: str, domainFull: str, originPathStr: str, optionsActor: str, @@ -202,9 +203,13 @@ def htmlPersonOptions(cssCache: {}, translate: {}, baseDir: str, optionsStr += checkboxStr optionsStr += optionsLinkStr + backPath = '/' + if nickname: + backPath = '/users/' + nickname + '/' + defaultTimeline optionsStr += \ - ' ' + ' ' optionsStr += \ ' ' diff --git a/webapp_suspended.py b/webapp_suspended.py new file mode 100644 index 000000000..cbcc1a23e --- /dev/null +++ b/webapp_suspended.py @@ -0,0 +1,31 @@ +__filename__ = "webapp_suspended.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.1.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + +import os +from utils import getCSS +from webapp_utils import htmlHeader +from webapp_utils import htmlFooter + + +def htmlSuspended(cssCache: {}, baseDir: str) -> str: + """Show the screen for suspended accounts + """ + suspendedForm = '' + cssFilename = baseDir + '/epicyon-suspended.css' + if os.path.isfile(baseDir + '/suspended.css'): + cssFilename = baseDir + '/suspended.css' + + suspendedCSS = getCSS(baseDir, cssFilename, cssCache) + if suspendedCSS: + suspendedForm = htmlHeader(cssFilename, suspendedCSS) + suspendedForm += '
\n' + suspendedForm += '

Account Suspended

\n' + suspendedForm += '

See Terms of Service

\n' + suspendedForm += '
\n' + suspendedForm += htmlFooter() + return suspendedForm diff --git a/webapp_timeline.py b/webapp_timeline.py index 9711814ce..9e65b3d64 100644 --- a/webapp_timeline.py +++ b/webapp_timeline.py @@ -18,7 +18,7 @@ from happening import thisWeeksEventsCheck from webapp_utils import getIconsDir from webapp_utils import htmlPostSeparator from webapp_utils import getBannerFile -from webapp_utils import htmlHeader +from webapp_utils import htmlHeaderWithExternalStyle from webapp_utils import htmlFooter from webapp_utils import sharesTimelineJson from webapp_post import preparePostFromHtmlCache @@ -29,6 +29,600 @@ from posts import isModerator from posts import isEditor +def htmlTimeline(cssCache: {}, defaultTimeline: str, + recentPostsCache: {}, maxRecentPosts: int, + translate: {}, pageNumber: int, + itemsPerPage: int, session, baseDir: str, + wfRequest: {}, personCache: {}, + nickname: str, domain: str, port: int, timelineJson: {}, + boxName: str, allowDeletion: bool, + httpPrefix: str, projectVersion: str, + manuallyApproveFollowers: bool, + minimal: bool, + YTReplacementDomain: str, + showPublishedDateOnly: bool, + newswire: {}, moderator: bool, + editor: bool, + positiveVoting: bool, + showPublishAsIcon: bool, + fullWidthTimelineButtonHeader: bool, + iconsAsButtons: bool, + rssIconAtTop: bool, + publishButtonAtTop: bool, + authorized: bool) -> str: + """Show the timeline as html + """ + timelineStartTime = time.time() + + accountDir = baseDir + '/accounts/' + nickname + '@' + domain + + # should the calendar icon be highlighted? + newCalendarEvent = False + calendarImage = 'calendar.png' + calendarPath = '/calendar' + calendarFile = accountDir + '/.newCalendar' + if os.path.isfile(calendarFile): + newCalendarEvent = True + calendarImage = 'calendar_notify.png' + with open(calendarFile, 'r') as calfile: + calendarPath = calfile.read().replace('##sent##', '') + calendarPath = calendarPath.replace('\n', '').replace('\r', '') + + # should the DM button be highlighted? + newDM = False + dmFile = accountDir + '/.newDM' + if os.path.isfile(dmFile): + newDM = True + if boxName == 'dm': + os.remove(dmFile) + + # should the Replies button be highlighted? + newReply = False + replyFile = accountDir + '/.newReply' + if os.path.isfile(replyFile): + newReply = True + if boxName == 'tlreplies': + os.remove(replyFile) + + # should the Shares button be highlighted? + newShare = False + newShareFile = accountDir + '/.newShare' + if os.path.isfile(newShareFile): + newShare = True + if boxName == 'tlshares': + os.remove(newShareFile) + + # should the Moderation/reports button be highlighted? + newReport = False + newReportFile = accountDir + '/.newReport' + if os.path.isfile(newReportFile): + newReport = True + if boxName == 'moderation': + os.remove(newReportFile) + + # directory where icons are found + # This changes depending upon theme + iconsDir = getIconsDir(baseDir) + + separatorStr = '' + if boxName != 'tlmedia': + separatorStr = htmlPostSeparator(baseDir, None) + + # the css filename + cssFilename = baseDir + '/epicyon-profile.css' + if os.path.isfile(baseDir + '/epicyon.css'): + cssFilename = baseDir + '/epicyon.css' + + # filename of the banner shown at the top + bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) + + # benchmark 1 + timeDiff = int((time.time() - timelineStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE TIMING ' + boxName + ' 1 = ' + str(timeDiff)) + + profileStyle = getCSS(baseDir, cssFilename, cssCache) + if not profileStyle: + print('ERROR: css file not found ' + cssFilename) + return None + + # replace any https within the css with whatever prefix is needed + if httpPrefix != 'https': + profileStyle = \ + profileStyle.replace('https://', + httpPrefix + '://') + + # is the user a moderator? + if not moderator: + moderator = isModerator(baseDir, nickname) + + # is the user a site editor? + if not editor: + editor = isEditor(baseDir, nickname) + + # benchmark 2 + timeDiff = int((time.time() - timelineStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE TIMING ' + boxName + ' 2 = ' + str(timeDiff)) + + # the appearance of buttons - highlighted or not + inboxButton = 'button' + blogsButton = 'button' + newsButton = 'button' + dmButton = 'button' + if newDM: + dmButton = 'buttonhighlighted' + repliesButton = 'button' + if newReply: + repliesButton = 'buttonhighlighted' + mediaButton = 'button' + bookmarksButton = 'button' + eventsButton = 'button' + sentButton = 'button' + sharesButton = 'button' + if newShare: + sharesButton = 'buttonhighlighted' + moderationButton = 'button' + if newReport: + moderationButton = 'buttonhighlighted' + if boxName == 'inbox': + inboxButton = 'buttonselected' + elif boxName == 'tlblogs': + blogsButton = 'buttonselected' + elif boxName == 'tlnews': + newsButton = 'buttonselected' + elif boxName == 'dm': + dmButton = 'buttonselected' + if newDM: + dmButton = 'buttonselectedhighlighted' + elif boxName == 'tlreplies': + repliesButton = 'buttonselected' + if newReply: + repliesButton = 'buttonselectedhighlighted' + elif boxName == 'tlmedia': + mediaButton = 'buttonselected' + elif boxName == 'outbox': + sentButton = 'buttonselected' + elif boxName == 'moderation': + moderationButton = 'buttonselected' + if newReport: + moderationButton = 'buttonselectedhighlighted' + elif boxName == 'tlshares': + sharesButton = 'buttonselected' + if newShare: + sharesButton = 'buttonselectedhighlighted' + elif boxName == 'tlbookmarks' or boxName == 'bookmarks': + bookmarksButton = 'buttonselected' + elif boxName == 'tlevents': + eventsButton = 'buttonselected' + + # get the full domain, including any port number + fullDomain = domain + if port != 80 and port != 443: + if ':' not in domain: + fullDomain = domain + ':' + str(port) + + usersPath = '/users/' + nickname + actor = httpPrefix + '://' + fullDomain + usersPath + + showIndividualPostIcons = True + + # show an icon for new follow approvals + followApprovals = '' + followRequestsFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + '/followrequests.txt' + if os.path.isfile(followRequestsFilename): + with open(followRequestsFilename, 'r') as f: + for line in f: + if len(line) > 0: + # show follow approvals icon + followApprovals = \ + '' + \ + '' + \
+                        translate['Approve follow requests'] + \
+                        '\n' + break + + # benchmark 3 + timeDiff = int((time.time() - timelineStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE TIMING ' + boxName + ' 3 = ' + str(timeDiff)) + + # moderation / reports button + moderationButtonStr = '' + if moderator and not minimal: + moderationButtonStr = \ + '' + + # shares, bookmarks and events buttons + sharesButtonStr = '' + bookmarksButtonStr = '' + eventsButtonStr = '' + if not minimal: + sharesButtonStr = \ + '' + + bookmarksButtonStr = \ + '' + + eventsButtonStr = \ + '' + + tlStr = htmlHeaderWithExternalStyle(cssFilename, profileStyle) + + # benchmark 4 + timeDiff = int((time.time() - timelineStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE TIMING ' + boxName + ' 4 = ' + str(timeDiff)) + + # if this is a news instance and we are viewing the news timeline + newsHeader = False + if defaultTimeline == 'tlnews' and boxName == 'tlnews': + newsHeader = True + + newPostButtonStr = '' + # start of headericons div + if not newsHeader: + if not iconsAsButtons: + newPostButtonStr += '
' + + # what screen to go to when a new post is created + if boxName == 'dm': + if not iconsAsButtons: + newPostButtonStr += \ + '| ' + translate['Create a new DM'] + \
+                '\n' + else: + newPostButtonStr += \ + '' + \ + '' + elif boxName == 'tlblogs' or boxName == 'tlnews': + if not iconsAsButtons: + newPostButtonStr += \ + '| ' + \
+                translate['Create a new post'] + \
+                '\n' + else: + newPostButtonStr += \ + '' + \ + '' + elif boxName == 'tlevents': + if not iconsAsButtons: + newPostButtonStr += \ + '| ' + \
+                translate['Create a new event'] + \
+                '\n' + else: + newPostButtonStr += \ + '' + \ + '' + else: + if not manuallyApproveFollowers: + if not iconsAsButtons: + newPostButtonStr += \ + '| ' + \
+                    translate['Create a new post'] + \
+                    '\n' + else: + newPostButtonStr += \ + '' + \ + '' + else: + if not iconsAsButtons: + newPostButtonStr += \ + '| ' + translate['Create a new post'] + \
+                    '\n' + else: + newPostButtonStr += \ + '' + \ + '' + + # This creates a link to the profile page when viewed + # in lynx, but should be invisible in a graphical web browser + tlStr += \ + '\n' + + # banner and row of buttons + tlStr += \ + '\n' + tlStr += '\n' + + if fullWidthTimelineButtonHeader: + tlStr += \ + headerButtonsTimeline(defaultTimeline, boxName, pageNumber, + translate, usersPath, mediaButton, + blogsButton, newsButton, inboxButton, + dmButton, newDM, repliesButton, + newReply, minimal, sentButton, + sharesButtonStr, bookmarksButtonStr, + eventsButtonStr, moderationButtonStr, + newPostButtonStr, baseDir, nickname, + domain, iconsDir, timelineStartTime, + newCalendarEvent, calendarPath, + calendarImage, followApprovals, + iconsAsButtons) + + # start the timeline + tlStr += '\n' + tlStr += ' \n' + tlStr += ' \n' + tlStr += ' \n' + tlStr += ' \n' + tlStr += ' \n' + tlStr += ' \n' + tlStr += ' \n' + + domainFull = domain + if port: + if port != 80 and port != 443: + domainFull = domain + ':' + str(port) + + # left column + leftColumnStr = \ + getLeftColumnContent(baseDir, nickname, domainFull, + httpPrefix, translate, iconsDir, + editor, False, None, rssIconAtTop, + True, False) + tlStr += ' \n' + # center column containing posts + tlStr += ' \n' + + # right column + rightColumnStr = getRightColumnContent(baseDir, nickname, domainFull, + httpPrefix, translate, iconsDir, + moderator, editor, + newswire, positiveVoting, + False, None, True, + showPublishAsIcon, + rssIconAtTop, publishButtonAtTop, + authorized, True) + tlStr += ' \n' + tlStr += ' \n' + + # benchmark 9 + timeDiff = int((time.time() - timelineStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE TIMING ' + boxName + ' 9 = ' + str(timeDiff)) + + tlStr += ' \n' + tlStr += '
' + \ + leftColumnStr + ' \n' + + if not fullWidthTimelineButtonHeader: + tlStr += \ + headerButtonsTimeline(defaultTimeline, boxName, pageNumber, + translate, usersPath, mediaButton, + blogsButton, newsButton, inboxButton, + dmButton, newDM, repliesButton, + newReply, minimal, sentButton, + sharesButtonStr, bookmarksButtonStr, + eventsButtonStr, moderationButtonStr, + newPostButtonStr, baseDir, nickname, + domain, iconsDir, timelineStartTime, + newCalendarEvent, calendarPath, + calendarImage, followApprovals, + iconsAsButtons) + + # second row of buttons for moderator actions + if moderator and boxName == 'moderation': + tlStr += \ + '
' + tlStr += '
\n' + idx = 'Nickname or URL. Block using *@domain or nickname@domain' + tlStr += \ + ' ' + translate[idx] + '
\n' + tlStr += '
\n' + tlStr += \ + ' \n' + tlStr += \ + ' \n' + tlStr += \ + ' \n' + tlStr += \ + ' \n' + tlStr += \ + ' \n' + tlStr += \ + ' \n' + tlStr += '
\n
\n' + + # benchmark 6 + timeDiff = int((time.time() - timelineStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE TIMING ' + boxName + ' 6 = ' + str(timeDiff)) + + if boxName == 'tlshares': + maxSharesPerAccount = itemsPerPage + return (tlStr + + htmlSharesTimeline(translate, pageNumber, itemsPerPage, + baseDir, actor, nickname, domain, port, + maxSharesPerAccount, httpPrefix) + + htmlFooter()) + + # benchmark 7 + timeDiff = int((time.time() - timelineStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE TIMING ' + boxName + ' 7 = ' + str(timeDiff)) + + # benchmark 8 + timeDiff = int((time.time() - timelineStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE TIMING ' + boxName + ' 8 = ' + str(timeDiff)) + + # page up arrow + if pageNumber > 1: + tlStr += \ + '
\n' + \ + ' ' + \
+            translate['Page up'] + '\n' + \ + '
\n' + + # show the posts + itemCtr = 0 + if timelineJson: + # if this is the media timeline then add an extra gallery container + if boxName == 'tlmedia': + if pageNumber > 1: + tlStr += '
' + tlStr += '
\n' + + # show each post in the timeline + for item in timelineJson['orderedItems']: + timelinePostStartTime = time.time() + + if item['type'] == 'Create' or \ + item['type'] == 'Announce' or \ + item['type'] == 'Update': + # is the actor who sent this post snoozed? + if isPersonSnoozed(baseDir, nickname, domain, item['actor']): + continue + + # is the post in the memory cache of recent ones? + currTlStr = None + if boxName != 'tlmedia' and \ + recentPostsCache.get('index'): + postId = \ + removeIdEnding(item['id']).replace('/', '#') + if postId in recentPostsCache['index']: + if not item.get('muted'): + if recentPostsCache['html'].get(postId): + currTlStr = recentPostsCache['html'][postId] + currTlStr = \ + preparePostFromHtmlCache(currTlStr, + boxName, + pageNumber) + # benchmark cache post + timeDiff = \ + int((time.time() - + timelinePostStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE POST CACHE TIMING ' + + boxName + ' = ' + str(timeDiff)) + + if not currTlStr: + # benchmark cache post + timeDiff = \ + int((time.time() - + timelinePostStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE POST DISK TIMING START ' + + boxName + ' = ' + str(timeDiff)) + + # read the post from disk + currTlStr = \ + individualPostAsHtml(False, recentPostsCache, + maxRecentPosts, + iconsDir, translate, pageNumber, + baseDir, session, wfRequest, + personCache, + nickname, domain, port, + item, None, True, + allowDeletion, + httpPrefix, projectVersion, + boxName, + YTReplacementDomain, + showPublishedDateOnly, + boxName != 'dm', + showIndividualPostIcons, + manuallyApproveFollowers, + False, True) + # benchmark cache post + timeDiff = \ + int((time.time() - + timelinePostStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE POST DISK TIMING ' + + boxName + ' = ' + str(timeDiff)) + + if currTlStr: + itemCtr += 1 + if separatorStr: + tlStr += separatorStr + tlStr += currTlStr + if boxName == 'tlmedia': + tlStr += '
\n' + + # page down arrow + if itemCtr > 2: + tlStr += \ + '
\n' + \ + ' ' + \
+            translate['Page down'] + '\n' + \ + '
\n' + + # end of column-center + tlStr += '
' + \ + rightColumnStr + '
\n' + tlStr += htmlFooter() + return tlStr + + def htmlIndividualShare(actor: str, item: {}, translate: {}, showContact: bool, removeButton: bool) -> str: """Returns an individual shared item as html @@ -440,600 +1034,6 @@ def headerButtonsTimeline(defaultTimeline: str, return tlStr -def htmlTimeline(cssCache: {}, defaultTimeline: str, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, pageNumber: int, - itemsPerPage: int, session, baseDir: str, - wfRequest: {}, personCache: {}, - nickname: str, domain: str, port: int, timelineJson: {}, - boxName: str, allowDeletion: bool, - httpPrefix: str, projectVersion: str, - manuallyApproveFollowers: bool, - minimal: bool, - YTReplacementDomain: str, - showPublishedDateOnly: bool, - newswire: {}, moderator: bool, - editor: bool, - positiveVoting: bool, - showPublishAsIcon: bool, - fullWidthTimelineButtonHeader: bool, - iconsAsButtons: bool, - rssIconAtTop: bool, - publishButtonAtTop: bool, - authorized: bool) -> str: - """Show the timeline as html - """ - timelineStartTime = time.time() - - accountDir = baseDir + '/accounts/' + nickname + '@' + domain - - # should the calendar icon be highlighted? - newCalendarEvent = False - calendarImage = 'calendar.png' - calendarPath = '/calendar' - calendarFile = accountDir + '/.newCalendar' - if os.path.isfile(calendarFile): - newCalendarEvent = True - calendarImage = 'calendar_notify.png' - with open(calendarFile, 'r') as calfile: - calendarPath = calfile.read().replace('##sent##', '') - calendarPath = calendarPath.replace('\n', '').replace('\r', '') - - # should the DM button be highlighted? - newDM = False - dmFile = accountDir + '/.newDM' - if os.path.isfile(dmFile): - newDM = True - if boxName == 'dm': - os.remove(dmFile) - - # should the Replies button be highlighted? - newReply = False - replyFile = accountDir + '/.newReply' - if os.path.isfile(replyFile): - newReply = True - if boxName == 'tlreplies': - os.remove(replyFile) - - # should the Shares button be highlighted? - newShare = False - newShareFile = accountDir + '/.newShare' - if os.path.isfile(newShareFile): - newShare = True - if boxName == 'tlshares': - os.remove(newShareFile) - - # should the Moderation/reports button be highlighted? - newReport = False - newReportFile = accountDir + '/.newReport' - if os.path.isfile(newReportFile): - newReport = True - if boxName == 'moderation': - os.remove(newReportFile) - - # directory where icons are found - # This changes depending upon theme - iconsDir = getIconsDir(baseDir) - - separatorStr = '' - if boxName != 'tlmedia': - separatorStr = htmlPostSeparator(baseDir, None) - - # the css filename - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - # filename of the banner shown at the top - bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) - - # benchmark 1 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 1 = ' + str(timeDiff)) - - profileStyle = getCSS(baseDir, cssFilename, cssCache) - if not profileStyle: - print('ERROR: css file not found ' + cssFilename) - return None - - # replace any https within the css with whatever prefix is needed - if httpPrefix != 'https': - profileStyle = \ - profileStyle.replace('https://', - httpPrefix + '://') - - # is the user a moderator? - if not moderator: - moderator = isModerator(baseDir, nickname) - - # is the user a site editor? - if not editor: - editor = isEditor(baseDir, nickname) - - # benchmark 2 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 2 = ' + str(timeDiff)) - - # the appearance of buttons - highlighted or not - inboxButton = 'button' - blogsButton = 'button' - newsButton = 'button' - dmButton = 'button' - if newDM: - dmButton = 'buttonhighlighted' - repliesButton = 'button' - if newReply: - repliesButton = 'buttonhighlighted' - mediaButton = 'button' - bookmarksButton = 'button' - eventsButton = 'button' - sentButton = 'button' - sharesButton = 'button' - if newShare: - sharesButton = 'buttonhighlighted' - moderationButton = 'button' - if newReport: - moderationButton = 'buttonhighlighted' - if boxName == 'inbox': - inboxButton = 'buttonselected' - elif boxName == 'tlblogs': - blogsButton = 'buttonselected' - elif boxName == 'tlnews': - newsButton = 'buttonselected' - elif boxName == 'dm': - dmButton = 'buttonselected' - if newDM: - dmButton = 'buttonselectedhighlighted' - elif boxName == 'tlreplies': - repliesButton = 'buttonselected' - if newReply: - repliesButton = 'buttonselectedhighlighted' - elif boxName == 'tlmedia': - mediaButton = 'buttonselected' - elif boxName == 'outbox': - sentButton = 'buttonselected' - elif boxName == 'moderation': - moderationButton = 'buttonselected' - if newReport: - moderationButton = 'buttonselectedhighlighted' - elif boxName == 'tlshares': - sharesButton = 'buttonselected' - if newShare: - sharesButton = 'buttonselectedhighlighted' - elif boxName == 'tlbookmarks' or boxName == 'bookmarks': - bookmarksButton = 'buttonselected' - elif boxName == 'tlevents': - eventsButton = 'buttonselected' - - # get the full domain, including any port number - fullDomain = domain - if port != 80 and port != 443: - if ':' not in domain: - fullDomain = domain + ':' + str(port) - - usersPath = '/users/' + nickname - actor = httpPrefix + '://' + fullDomain + usersPath - - showIndividualPostIcons = True - - # show an icon for new follow approvals - followApprovals = '' - followRequestsFilename = \ - baseDir + '/accounts/' + \ - nickname + '@' + domain + '/followrequests.txt' - if os.path.isfile(followRequestsFilename): - with open(followRequestsFilename, 'r') as f: - for line in f: - if len(line) > 0: - # show follow approvals icon - followApprovals = \ - '' + \ - '' + \
-                        translate['Approve follow requests'] + \
-                        '\n' - break - - # benchmark 3 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 3 = ' + str(timeDiff)) - - # moderation / reports button - moderationButtonStr = '' - if moderator and not minimal: - moderationButtonStr = \ - '' - - # shares, bookmarks and events buttons - sharesButtonStr = '' - bookmarksButtonStr = '' - eventsButtonStr = '' - if not minimal: - sharesButtonStr = \ - '' - - bookmarksButtonStr = \ - '' - - eventsButtonStr = \ - '' - - tlStr = htmlHeader(cssFilename, profileStyle) - - # benchmark 4 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 4 = ' + str(timeDiff)) - - # if this is a news instance and we are viewing the news timeline - newsHeader = False - if defaultTimeline == 'tlnews' and boxName == 'tlnews': - newsHeader = True - - newPostButtonStr = '' - # start of headericons div - if not newsHeader: - if not iconsAsButtons: - newPostButtonStr += '
' - - # what screen to go to when a new post is created - if boxName == 'dm': - if not iconsAsButtons: - newPostButtonStr += \ - '| ' + translate['Create a new DM'] + \
-                '\n' - else: - newPostButtonStr += \ - '' + \ - '' - elif boxName == 'tlblogs' or boxName == 'tlnews': - if not iconsAsButtons: - newPostButtonStr += \ - '| ' + \
-                translate['Create a new post'] + \
-                '\n' - else: - newPostButtonStr += \ - '' + \ - '' - elif boxName == 'tlevents': - if not iconsAsButtons: - newPostButtonStr += \ - '| ' + \
-                translate['Create a new event'] + \
-                '\n' - else: - newPostButtonStr += \ - '' + \ - '' - else: - if not manuallyApproveFollowers: - if not iconsAsButtons: - newPostButtonStr += \ - '| ' + \
-                    translate['Create a new post'] + \
-                    '\n' - else: - newPostButtonStr += \ - '' + \ - '' - else: - if not iconsAsButtons: - newPostButtonStr += \ - '| ' + translate['Create a new post'] + \
-                    '\n' - else: - newPostButtonStr += \ - '' + \ - '' - - # This creates a link to the profile page when viewed - # in lynx, but should be invisible in a graphical web browser - tlStr += \ - '\n' - - # banner and row of buttons - tlStr += \ - '\n' - tlStr += '\n' - - if fullWidthTimelineButtonHeader: - tlStr += \ - headerButtonsTimeline(defaultTimeline, boxName, pageNumber, - translate, usersPath, mediaButton, - blogsButton, newsButton, inboxButton, - dmButton, newDM, repliesButton, - newReply, minimal, sentButton, - sharesButtonStr, bookmarksButtonStr, - eventsButtonStr, moderationButtonStr, - newPostButtonStr, baseDir, nickname, - domain, iconsDir, timelineStartTime, - newCalendarEvent, calendarPath, - calendarImage, followApprovals, - iconsAsButtons) - - # start the timeline - tlStr += '\n' - tlStr += ' \n' - tlStr += ' \n' - tlStr += ' \n' - tlStr += ' \n' - tlStr += ' \n' - tlStr += ' \n' - tlStr += ' \n' - - domainFull = domain - if port: - if port != 80 and port != 443: - domainFull = domain + ':' + str(port) - - # left column - leftColumnStr = \ - getLeftColumnContent(baseDir, nickname, domainFull, - httpPrefix, translate, iconsDir, - editor, False, None, rssIconAtTop, - True, False) - tlStr += ' \n' - # center column containing posts - tlStr += ' \n' - - # right column - rightColumnStr = getRightColumnContent(baseDir, nickname, domainFull, - httpPrefix, translate, iconsDir, - moderator, editor, - newswire, positiveVoting, - False, None, True, - showPublishAsIcon, - rssIconAtTop, publishButtonAtTop, - authorized, True) - tlStr += ' \n' - tlStr += ' \n' - - # benchmark 9 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 9 = ' + str(timeDiff)) - - tlStr += ' \n' - tlStr += '
' + \ - leftColumnStr + ' \n' - - if not fullWidthTimelineButtonHeader: - tlStr += \ - headerButtonsTimeline(defaultTimeline, boxName, pageNumber, - translate, usersPath, mediaButton, - blogsButton, newsButton, inboxButton, - dmButton, newDM, repliesButton, - newReply, minimal, sentButton, - sharesButtonStr, bookmarksButtonStr, - eventsButtonStr, moderationButtonStr, - newPostButtonStr, baseDir, nickname, - domain, iconsDir, timelineStartTime, - newCalendarEvent, calendarPath, - calendarImage, followApprovals, - iconsAsButtons) - - # second row of buttons for moderator actions - if moderator and boxName == 'moderation': - tlStr += \ - '
' - tlStr += '
\n' - idx = 'Nickname or URL. Block using *@domain or nickname@domain' - tlStr += \ - ' ' + translate[idx] + '
\n' - tlStr += '
\n' - tlStr += \ - ' \n' - tlStr += \ - ' \n' - tlStr += \ - ' \n' - tlStr += \ - ' \n' - tlStr += \ - ' \n' - tlStr += \ - ' \n' - tlStr += '
\n
\n' - - # benchmark 6 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 6 = ' + str(timeDiff)) - - if boxName == 'tlshares': - maxSharesPerAccount = itemsPerPage - return (tlStr + - htmlSharesTimeline(translate, pageNumber, itemsPerPage, - baseDir, actor, nickname, domain, port, - maxSharesPerAccount, httpPrefix) + - htmlFooter()) - - # benchmark 7 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 7 = ' + str(timeDiff)) - - # benchmark 8 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 8 = ' + str(timeDiff)) - - # page up arrow - if pageNumber > 1: - tlStr += \ - '
\n' + \ - ' ' + \
-            translate['Page up'] + '\n' + \ - '
\n' - - # show the posts - itemCtr = 0 - if timelineJson: - # if this is the media timeline then add an extra gallery container - if boxName == 'tlmedia': - if pageNumber > 1: - tlStr += '
' - tlStr += '
\n' - - # show each post in the timeline - for item in timelineJson['orderedItems']: - timelinePostStartTime = time.time() - - if item['type'] == 'Create' or \ - item['type'] == 'Announce' or \ - item['type'] == 'Update': - # is the actor who sent this post snoozed? - if isPersonSnoozed(baseDir, nickname, domain, item['actor']): - continue - - # is the post in the memory cache of recent ones? - currTlStr = None - if boxName != 'tlmedia' and \ - recentPostsCache.get('index'): - postId = \ - removeIdEnding(item['id']).replace('/', '#') - if postId in recentPostsCache['index']: - if not item.get('muted'): - if recentPostsCache['html'].get(postId): - currTlStr = recentPostsCache['html'][postId] - currTlStr = \ - preparePostFromHtmlCache(currTlStr, - boxName, - pageNumber) - # benchmark cache post - timeDiff = \ - int((time.time() - - timelinePostStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE POST CACHE TIMING ' + - boxName + ' = ' + str(timeDiff)) - - if not currTlStr: - # benchmark cache post - timeDiff = \ - int((time.time() - - timelinePostStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE POST DISK TIMING START ' + - boxName + ' = ' + str(timeDiff)) - - # read the post from disk - currTlStr = \ - individualPostAsHtml(False, recentPostsCache, - maxRecentPosts, - iconsDir, translate, pageNumber, - baseDir, session, wfRequest, - personCache, - nickname, domain, port, - item, None, True, - allowDeletion, - httpPrefix, projectVersion, - boxName, - YTReplacementDomain, - showPublishedDateOnly, - boxName != 'dm', - showIndividualPostIcons, - manuallyApproveFollowers, - False, True) - # benchmark cache post - timeDiff = \ - int((time.time() - - timelinePostStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE POST DISK TIMING ' + - boxName + ' = ' + str(timeDiff)) - - if currTlStr: - itemCtr += 1 - if separatorStr: - tlStr += separatorStr - tlStr += currTlStr - if boxName == 'tlmedia': - tlStr += '
\n' - - # page down arrow - if itemCtr > 2: - tlStr += \ - '
\n' + \ - ' ' + \
-            translate['Page down'] + '\n' + \ - '
\n' - - # end of column-center - tlStr += '
' + \ - rightColumnStr + '
\n' - tlStr += htmlFooter() - return tlStr - - def htmlShares(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, @@ -1346,37 +1346,6 @@ def htmlInboxNews(cssCache: {}, defaultTimeline: str, authorized) -def htmlModeration(cssCache: {}, defaultTimeline: str, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, pageNumber: int, itemsPerPage: int, - session, baseDir: str, wfRequest: {}, personCache: {}, - nickname: str, domain: str, port: int, inboxJson: {}, - allowDeletion: bool, - httpPrefix: str, projectVersion: str, - YTReplacementDomain: str, - showPublishedDateOnly: bool, - newswire: {}, positiveVoting: bool, - showPublishAsIcon: bool, - fullWidthTimelineButtonHeader: bool, - iconsAsButtons: bool, - rssIconAtTop: bool, - publishButtonAtTop: bool, - authorized: bool) -> str: - """Show the moderation feed as html - """ - return htmlTimeline(cssCache, defaultTimeline, - recentPostsCache, maxRecentPosts, - translate, pageNumber, - itemsPerPage, session, baseDir, wfRequest, personCache, - nickname, domain, port, inboxJson, 'moderation', - allowDeletion, httpPrefix, projectVersion, True, False, - YTReplacementDomain, showPublishedDateOnly, - newswire, False, False, positiveVoting, - showPublishAsIcon, fullWidthTimelineButtonHeader, - iconsAsButtons, rssIconAtTop, publishButtonAtTop, - authorized) - - def htmlOutbox(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, diff --git a/webapp_tos.py b/webapp_tos.py new file mode 100644 index 000000000..707a29d42 --- /dev/null +++ b/webapp_tos.py @@ -0,0 +1,57 @@ +__filename__ = "webapp_tos.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 getCSS +from utils import getConfigParam +from webapp_utils import htmlHeader +from webapp_utils import htmlFooter + + +def htmlTermsOfService(cssCache: {}, baseDir: str, + httpPrefix: str, domainFull: str) -> str: + """Show the terms of service screen + """ + adminNickname = getConfigParam(baseDir, 'admin') + if not os.path.isfile(baseDir + '/accounts/tos.txt'): + copyfile(baseDir + '/default_tos.txt', + baseDir + '/accounts/tos.txt') + + if os.path.isfile(baseDir + '/accounts/login-background-custom.jpg'): + if not os.path.isfile(baseDir + '/accounts/login-background.jpg'): + copyfile(baseDir + '/accounts/login-background-custom.jpg', + baseDir + '/accounts/login-background.jpg') + + TOSText = 'Terms of Service go here.' + if os.path.isfile(baseDir + '/accounts/tos.txt'): + with open(baseDir + '/accounts/tos.txt', 'r') as file: + TOSText = file.read() + + TOSForm = '' + cssFilename = baseDir + '/epicyon-profile.css' + if os.path.isfile(baseDir + '/epicyon.css'): + cssFilename = baseDir + '/epicyon.css' + + termsCSS = getCSS(baseDir, cssFilename, cssCache) + if termsCSS: + if httpPrefix != 'https': + termsCSS = termsCSS.replace('https://', httpPrefix+'://') + + TOSForm = htmlHeader(cssFilename, termsCSS) + TOSForm += '
' + TOSText + '
\n' + if adminNickname: + adminActor = httpPrefix + '://' + domainFull + \ + '/users/' + adminNickname + TOSForm += \ + '
\n' + \ + '

Administered by ' + adminNickname + '

\n' + \ + '
\n' + TOSForm += htmlFooter() + return TOSForm diff --git a/webapp_utils.py b/webapp_utils.py index 2dcf172c0..8e5894fe1 100644 --- a/webapp_utils.py +++ b/webapp_utils.py @@ -437,6 +437,25 @@ def htmlHeader(cssFilename: str, css: str, lang='en') -> str: return htmlStr +def htmlHeaderWithExternalStyle(cssFilename: str, css: str, lang='en') -> str: + htmlStr = '\n' + htmlStr += '\n' + htmlStr += ' \n' + htmlStr += ' \n' + fontName, fontFormat = getFontFromCss(css) + if fontName: + htmlStr += ' \n' + cssFile = cssFilename.split('/')[-1] + htmlStr += ' \n' + htmlStr += ' \n' + htmlStr += ' \n' + htmlStr += ' Epicyon\n' + htmlStr += ' \n' + htmlStr += ' \n' + return htmlStr + + def htmlFooter() -> str: htmlStr = ' \n' htmlStr += '\n' diff --git a/website/EN/index.html b/website/EN/index.html index 3a8023cfb..e4b6be5cc 100644 --- a/website/EN/index.html +++ b/website/EN/index.html @@ -1168,7 +1168,7 @@ hacker theme profile page - various screenshots + various screenshots mobile screenshot @@ -1199,9 +1199,8 @@ RSS 2.0 - RSS 3.0

- You don't need a separate blog system. Blog posts can be added and edited, and are federated as ActivityPub articles. They also have RSS version 2.0 and 3.0 feeds. People can comment on blog posts, but unlike other systems the moderation settings apply just the same as they do for any other fediverse post arriving at your server. This makes blog spam much easier to keep control over. + You don't need a separate blog system. Blog posts can be added and edited, and are federated as ActivityPub articles. They also have RSS version 2.0 feeds. People can comment on blog posts, but unlike other systems the moderation settings apply just the same as they do for any other fediverse post arriving at your server. This makes blog spam much easier to keep control over. You can subscribe to other people's blogs and they will appear in the right hand newswire column.