Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon into main

main
Bob Mottram 2020-10-04 16:14:25 +01:00
commit 4d385abaf1
32 changed files with 3548 additions and 229 deletions

View File

@ -66,7 +66,7 @@ def createReject(baseDir: str, federationList: [],
return createAcceptReject(baseDir, federationList,
nickname, domain, port,
toUrl, ccUrl,
httpPrefix, objectJson, None, 'Reject')
httpPrefix, objectJson, 'Reject')
def acceptFollow(baseDir: str, domain: str, messageJson: {},

49
blog.py
View File

@ -20,6 +20,8 @@ from utils import getDomainFromActor
from utils import locatePost
from utils import loadJson
from posts import createBlogsTimeline
from newswire import rss2Header
from newswire import rss2Footer
def noOfBlogReplies(baseDir: str, httpPrefix: str, translate: {},
@ -365,12 +367,12 @@ def htmlBlogPost(authorized: bool,
'title="RSS 2.0" src="/' + \
iconsDir + '/rss.png" /></a>'
blogStr += '<a href="' + httpPrefix + '://' + \
domainFull + '/blog/' + nickname + '/rss.txt">'
blogStr += '<img style="width:3%;min-width:50px" ' + \
'loading="lazy" alt="RSS 3.0" ' + \
'title="RSS 3.0" src="/' + \
iconsDir + '/rss3.png" /></a>'
# blogStr += '<a href="' + httpPrefix + '://' + \
# domainFull + '/blog/' + nickname + '/rss.txt">'
# blogStr += '<img style="width:3%;min-width:50px" ' + \
# 'loading="lazy" alt="RSS 3.0" ' + \
# 'title="RSS 3.0" src="/' + \
# iconsDir + '/rss3.png" /></a>'
blogStr += '</p>'
@ -405,7 +407,7 @@ def htmlBlogPage(authorized: bool, session,
timelineJson = createBlogsTimeline(session, baseDir,
nickname, domain, port,
httpPrefix,
noOfItems, False, False,
noOfItems, False,
pageNumber)
if not timelineJson:
@ -461,11 +463,11 @@ def htmlBlogPage(authorized: bool, session,
'title="RSS 2.0" src="/' + \
iconsDir + '/rss.png" /></a>'
blogStr += '<a href="' + httpPrefix + '://' + \
domainFull + '/blog/' + nickname + '/rss.txt">'
blogStr += '<img loading="lazy" alt="RSS 3.0" ' + \
'title="RSS 3.0" src="/' + \
iconsDir + '/rss3.png" /></a>'
# blogStr += '<a href="' + httpPrefix + '://' + \
# domainFull + '/blog/' + nickname + '/rss.txt">'
# blogStr += '<img loading="lazy" alt="RSS 3.0" ' + \
# 'title="RSS 3.0" src="/' + \
# iconsDir + '/rss3.png" /></a>'
blogStr += '</p>'
@ -473,23 +475,6 @@ def htmlBlogPage(authorized: bool, session,
return None
def rss2Header(httpPrefix: str,
nickname: str, domainFull: str, translate: {}) -> str:
rssStr = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"
rssStr += "<rss version=\"2.0\">"
rssStr += '<channel>'
rssStr += ' <title>' + translate['Blog'] + '</title>'
rssStr += ' <link>' + httpPrefix + '://' + domainFull + \
'/users/' + nickname + '/rss.xml' + '</link>'
return rssStr
def rss2Footer() -> str:
rssStr = '</channel>'
rssStr += '</rss>'
return rssStr
def htmlBlogPageRSS2(authorized: bool, session,
baseDir: str, httpPrefix: str, translate: {},
nickname: str, domain: str, port: int,
@ -505,7 +490,7 @@ def htmlBlogPageRSS2(authorized: bool, session,
if port != 80 and port != 443:
domainFull = domain + ':' + str(port)
blogRSS2 = rss2Header(httpPrefix, nickname, domainFull, translate)
blogRSS2 = rss2Header(httpPrefix, nickname, domainFull, 'Blog', translate)
blogsIndex = baseDir + '/accounts/' + \
nickname + '@' + domain + '/tlblogs.index'
@ -515,7 +500,7 @@ def htmlBlogPageRSS2(authorized: bool, session,
timelineJson = createBlogsTimeline(session, baseDir,
nickname, domain, port,
httpPrefix,
noOfItems, False, False,
noOfItems, False,
pageNumber)
if not timelineJson:
@ -561,7 +546,7 @@ def htmlBlogPageRSS3(authorized: bool, session,
timelineJson = createBlogsTimeline(session, baseDir,
nickname, domain, port,
httpPrefix,
noOfItems, False, False,
noOfItems, False,
pageNumber)
if not timelineJson:

View File

@ -14,6 +14,32 @@ from utils import fileLastModified
from utils import getLinkPrefixes
def removeQuotesWithinQuotes(content: str) -> str:
"""Removes any blockquote inside blockquote
"""
if '<blockquote>' not in content:
return content
if '</blockquote>' not in content:
return content
ctr = 1
found = True
while found:
prefix = content.split('<blockquote>', ctr)[0] + '<blockquote>'
quotedStr = content.split('<blockquote>', ctr)[1]
if '</blockquote>' not in quotedStr:
found = False
else:
endStr = quotedStr.split('</blockquote>')[1]
quotedStr = quotedStr.split('</blockquote>')[0]
if '<blockquote>' not in endStr:
found = False
if '<blockquote>' in quotedStr:
quotedStr = quotedStr.replace('<blockquote>', '')
content = prefix + quotedStr + '</blockquote>' + endStr
ctr += 1
return content
def htmlReplaceEmailQuote(content: str) -> str:
"""Replaces an email style quote "> Some quote" with html blockquote
"""
@ -44,9 +70,12 @@ def htmlReplaceEmailQuote(content: str) -> str:
newContent += '<p>' + lineStr + '</p>'
else:
lineStr = lineStr.replace('>&gt; ', '><blockquote>')
if lineStr.startswith('&gt;'):
lineStr = lineStr.replace('&gt;', '<blockquote>', 1)
else:
lineStr = lineStr.replace('&gt;', '<br>')
newContent += '<p>' + lineStr + '</blockquote></p>'
return newContent
return removeQuotesWithinQuotes(newContent)
def htmlReplaceQuoteMarks(content: str) -> str:

424
daemon.py
View File

@ -61,6 +61,7 @@ from person import removeAccount
from person import canRemovePost
from person import personSnooze
from person import personUnsnooze
from posts import isModerator
from posts import mutePost
from posts import unmutePost
from posts import createQuestionPost
@ -144,6 +145,8 @@ from webinterface import htmlSearchEmojiTextEntry
from webinterface import htmlUnfollowConfirm
from webinterface import htmlProfileAfterSearch
from webinterface import htmlEditProfile
from webinterface import htmlEditLinks
from webinterface import htmlEditNewswire
from webinterface import htmlTermsOfService
from webinterface import htmlSkillsSearch
from webinterface import htmlHistorySearch
@ -200,6 +203,7 @@ from followingCalendar import removePersonFromCalendar
from devices import E2EEdevicesCollection
from devices import E2EEvalidDevice
from devices import E2EEaddDevice
from newswire import getRSSfromDict
import os
@ -2689,6 +2693,216 @@ class PubServer(BaseHTTPRequestHandler):
cookie, callingDomain)
self.server.POSTbusy = False
def _linksUpdate(self, callingDomain: str, cookie: str,
authorized: bool, path: str,
baseDir: str, httpPrefix: str,
domain: str, domainFull: str,
onionDomain: str, i2pDomain: str, debug: bool,
defaultTimeline: str):
"""Updates the left links column of the timeline
"""
usersPath = path.replace('/linksdata', '')
usersPath = usersPath.replace('/editlinks', '')
actorStr = httpPrefix + '://' + domainFull + usersPath
if ' boundary=' in self.headers['Content-type']:
boundary = self.headers['Content-type'].split('boundary=')[1]
if ';' in boundary:
boundary = boundary.split(';')[0]
# get the nickname
nickname = getNicknameFromActor(actorStr)
moderator = None
if nickname:
moderator = isModerator(baseDir, nickname)
if not nickname or not moderator:
if callingDomain.endswith('.onion') and \
onionDomain:
actorStr = \
'http://' + onionDomain + usersPath
elif (callingDomain.endswith('.i2p') and
i2pDomain):
actorStr = \
'http://' + i2pDomain + usersPath
if not nickname:
print('WARN: nickname not found in ' + actorStr)
else:
print('WARN: nickname is not a moderator' + actorStr)
self._redirect_headers(actorStr, cookie, callingDomain)
self.server.POSTbusy = False
return
length = int(self.headers['Content-length'])
# check that the POST isn't too large
if length > self.server.maxPostLength:
if callingDomain.endswith('.onion') and \
onionDomain:
actorStr = \
'http://' + onionDomain + usersPath
elif (callingDomain.endswith('.i2p') and
i2pDomain):
actorStr = \
'http://' + i2pDomain + usersPath
print('Maximum links data length exceeded ' + str(length))
self._redirect_headers(actorStr, cookie, callingDomain)
self.server.POSTbusy = False
return
try:
# read the bytes of the http form POST
postBytes = self.rfile.read(length)
except SocketError as e:
if e.errno == errno.ECONNRESET:
print('WARN: connection was reset while ' +
'reading bytes from http form POST')
else:
print('WARN: error while reading bytes ' +
'from http form POST')
self.send_response(400)
self.end_headers()
self.server.POSTbusy = False
return
except ValueError as e:
print('ERROR: failed to read bytes for POST')
print(e)
self.send_response(400)
self.end_headers()
self.server.POSTbusy = False
return
linksFilename = baseDir + '/accounts/links.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+")
if linksFile:
linksFile.write(linksStr)
linksFile.close()
else:
if os.path.isfile(linksFilename):
os.remove(linksFilename)
# redirect back to the default timeline
if callingDomain.endswith('.onion') and \
onionDomain:
actorStr = \
'http://' + onionDomain + usersPath
elif (callingDomain.endswith('.i2p') and
i2pDomain):
actorStr = \
'http://' + i2pDomain + usersPath
self._redirect_headers(actorStr + '/' + defaultTimeline,
cookie, callingDomain)
self.server.POSTbusy = False
def _newswireUpdate(self, callingDomain: str, cookie: str,
authorized: bool, path: str,
baseDir: str, httpPrefix: str,
domain: str, domainFull: str,
onionDomain: str, i2pDomain: str, debug: bool,
defaultTimeline: str):
"""Updates the right newswire column of the timeline
"""
usersPath = path.replace('/newswiredata', '')
usersPath = usersPath.replace('/editnewswire', '')
actorStr = httpPrefix + '://' + domainFull + usersPath
if ' boundary=' in self.headers['Content-type']:
boundary = self.headers['Content-type'].split('boundary=')[1]
if ';' in boundary:
boundary = boundary.split(';')[0]
# get the nickname
nickname = getNicknameFromActor(actorStr)
moderator = None
if nickname:
moderator = isModerator(baseDir, nickname)
if not nickname or not moderator:
if callingDomain.endswith('.onion') and \
onionDomain:
actorStr = \
'http://' + onionDomain + usersPath
elif (callingDomain.endswith('.i2p') and
i2pDomain):
actorStr = \
'http://' + i2pDomain + usersPath
if not nickname:
print('WARN: nickname not found in ' + actorStr)
else:
print('WARN: nickname is not a moderator' + actorStr)
self._redirect_headers(actorStr, cookie, callingDomain)
self.server.POSTbusy = False
return
length = int(self.headers['Content-length'])
# check that the POST isn't too large
if length > self.server.maxPostLength:
if callingDomain.endswith('.onion') and \
onionDomain:
actorStr = \
'http://' + onionDomain + usersPath
elif (callingDomain.endswith('.i2p') and
i2pDomain):
actorStr = \
'http://' + i2pDomain + usersPath
print('Maximum newswire data length exceeded ' + str(length))
self._redirect_headers(actorStr, cookie, callingDomain)
self.server.POSTbusy = False
return
try:
# read the bytes of the http form POST
postBytes = self.rfile.read(length)
except SocketError as e:
if e.errno == errno.ECONNRESET:
print('WARN: connection was reset while ' +
'reading bytes from http form POST')
else:
print('WARN: error while reading bytes ' +
'from http form POST')
self.send_response(400)
self.end_headers()
self.server.POSTbusy = False
return
except ValueError as e:
print('ERROR: failed to read bytes for POST')
print(e)
self.send_response(400)
self.end_headers()
self.server.POSTbusy = False
return
newswireFilename = baseDir + '/accounts/newswire.txt'
# extract all of the text fields into a dict
fields = \
extractTextFieldsInPOST(postBytes, boundary, debug)
if fields.get('editedNewswire'):
newswireStr = fields['editedNewswire']
newswireFile = open(newswireFilename, "w+")
if newswireFile:
newswireFile.write(newswireStr)
newswireFile.close()
else:
if os.path.isfile(newswireFilename):
os.remove(newswireFilename)
# redirect back to the default timeline
if callingDomain.endswith('.onion') and \
onionDomain:
actorStr = \
'http://' + onionDomain + usersPath
elif (callingDomain.endswith('.i2p') and
i2pDomain):
actorStr = \
'http://' + i2pDomain + usersPath
self._redirect_headers(actorStr + '/' + defaultTimeline,
cookie, callingDomain)
self.server.POSTbusy = False
def _profileUpdate(self, callingDomain: str, cookie: str,
authorized: bool, path: str,
baseDir: str, httpPrefix: str,
@ -2765,7 +2979,8 @@ class PubServer(BaseHTTPRequestHandler):
actorChanged = True
profileMediaTypes = ('avatar', 'image',
'banner', 'search_banner',
'instanceLogo')
'instanceLogo',
'left_col_image', 'right_col_image')
profileMediaTypesUploaded = {}
for mType in profileMediaTypes:
if debug:
@ -2834,8 +3049,7 @@ class PubServer(BaseHTTPRequestHandler):
# extract all of the text fields into a dict
fields = \
extractTextFieldsInPOST(postBytes, boundary,
debug)
extractTextFieldsInPOST(postBytes, boundary, debug)
if debug:
if fields:
print('DEBUG: profile update text ' +
@ -3205,10 +3419,14 @@ class PubServer(BaseHTTPRequestHandler):
self.server.defaultTimeline = 'inbox'
if fields['mediaInstance'] == 'on':
self.server.mediaInstance = True
self.server.blogsInstance = False
self.server.defaultTimeline = 'tlmedia'
setConfigParam(baseDir,
"mediaInstance",
self.server.mediaInstance)
setConfigParam(baseDir,
"blogsInstance",
self.server.blogsInstance)
else:
if self.server.mediaInstance:
self.server.mediaInstance = False
@ -3223,10 +3441,14 @@ class PubServer(BaseHTTPRequestHandler):
self.server.defaultTimeline = 'inbox'
if fields['blogsInstance'] == 'on':
self.server.blogsInstance = True
self.server.mediaInstance = False
self.server.defaultTimeline = 'tlblogs'
setConfigParam(baseDir,
"blogsInstance",
self.server.blogsInstance)
setConfigParam(baseDir,
"mediaInstance",
self.server.mediaInstance)
else:
if self.server.blogsInstance:
self.server.blogsInstance = False
@ -3733,6 +3955,42 @@ class PubServer(BaseHTTPRequestHandler):
path + ' ' + callingDomain)
self._404()
def _getNewswireFeed(self, authorized: bool,
callingDomain: str, path: str,
baseDir: str, httpPrefix: str,
domain: str, port: int, proxyType: str,
GETstartTime, GETtimings: {},
debug: bool):
"""Returns the newswire feed
"""
if not self.server.session:
print('Starting new session during RSS request')
self.server.session = \
createSession(proxyType)
if not self.server.session:
print('ERROR: GET failed to create session ' +
'during RSS request')
self._404()
return
msg = getRSSfromDict(self.server.baseDir, self.server.newswire,
self.server.httpPrefix,
self.server.domainFull,
'Newswire', self.server.translate)
if msg:
msg = msg.encode('utf-8')
self._set_headers('text/xml', len(msg),
None, callingDomain)
self._write(msg)
if debug:
print('Sent rss2 newswire feed: ' +
path + ' ' + callingDomain)
return
if debug:
print('Failed to get rss2 newswire feed: ' +
path + ' ' + callingDomain)
self._404()
def _getRSS3feed(self, authorized: bool,
callingDomain: str, path: str,
baseDir: str, httpPrefix: str,
@ -7047,6 +7305,47 @@ class PubServer(BaseHTTPRequestHandler):
self._404()
return True
def _columImage(self, side: str, callingDomain: str, path: str,
baseDir: str, domain: str, port: int,
GETstartTime, GETtimings: {}) -> bool:
"""Shows an image at the top of the left/right column
"""
nickname = getNicknameFromActor(path)
if not nickname:
self._404()
return True
bannerFilename = \
baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + side + '_col_image.png'
if os.path.isfile(bannerFilename):
if self._etag_exists(bannerFilename):
# The file has not changed
self._304()
return True
tries = 0
mediaBinary = None
while tries < 5:
try:
with open(bannerFilename, 'rb') as avFile:
mediaBinary = avFile.read()
break
except Exception as e:
print(e)
time.sleep(1)
tries += 1
if mediaBinary:
self._set_headers_etag(bannerFilename, 'image/png',
mediaBinary, None,
callingDomain)
self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'account qrcode done',
side + ' col image')
return True
self._404()
return True
def _showBackgroundImage(self, callingDomain: str, path: str,
baseDir: str,
GETstartTime, GETtimings: {}) -> bool:
@ -7307,6 +7606,50 @@ class PubServer(BaseHTTPRequestHandler):
return True
return False
def _editLinks(self, callingDomain: str, path: str,
translate: {}, baseDir: str,
httpPrefix: str, domain: str, port: int,
cookie: str) -> bool:
"""Show the links from the left column
"""
if '/users/' in path and path.endswith('/editlinks'):
msg = htmlEditLinks(translate,
baseDir,
path, domain,
port,
httpPrefix).encode('utf-8')
if msg:
self._set_headers('text/html', len(msg),
cookie, callingDomain)
self._write(msg)
else:
self._404()
self.server.GETbusy = False
return True
return False
def _editNewswire(self, callingDomain: str, path: str,
translate: {}, baseDir: str,
httpPrefix: str, domain: str, port: int,
cookie: str) -> bool:
"""Show the newswire from the right column
"""
if '/users/' in path and path.endswith('/editnewswire'):
msg = htmlEditNewswire(translate,
baseDir,
path, domain,
port,
httpPrefix).encode('utf-8')
if msg:
self._set_headers('text/html', len(msg),
cookie, callingDomain)
self._write(msg)
else:
self._404()
self.server.GETbusy = False
return True
return False
def _editEvent(self, callingDomain: str, path: str,
httpPrefix: str, domain: str, domainFull: str,
baseDir: str, translate: {},
@ -7507,6 +7850,18 @@ class PubServer(BaseHTTPRequestHandler):
self._benchmarkGETtimings(GETstartTime, GETtimings,
'fonts', 'sharedInbox enabled')
if self.path == '/newswire.xml':
self._getNewswireFeed(authorized,
callingDomain, self.path,
self.server.baseDir,
self.server.httpPrefix,
self.server.domain,
self.server.port,
self.server.proxyType,
GETstartTime, GETtimings,
self.server.debug)
return
# RSS 2.0
if self.path.startswith('/blog/') and \
self.path.endswith('/rss.xml'):
@ -7965,8 +8320,8 @@ class PubServer(BaseHTTPRequestHandler):
'account qrcode done')
# search screen banner image
if '/users/' in self.path and \
self.path.endswith('/search_banner.png'):
if '/users/' in self.path:
if self.path.endswith('/search_banner.png'):
if self._searchScreenBanner(callingDomain, self.path,
self.server.baseDir,
self.server.domain,
@ -7974,6 +8329,22 @@ class PubServer(BaseHTTPRequestHandler):
GETstartTime, GETtimings):
return
if self.path.endswith('/left_col_image.png'):
if self._columImage('left', callingDomain, self.path,
self.server.baseDir,
self.server.domain,
self.server.port,
GETstartTime, GETtimings):
return
if self.path.endswith('/right_col_image.png'):
if self._columImage('right', callingDomain, self.path,
self.server.baseDir,
self.server.domain,
self.server.port,
GETstartTime, GETtimings):
return
self._benchmarkGETtimings(GETstartTime, GETtimings,
'account qrcode done',
'search screen banner done')
@ -8601,6 +8972,26 @@ class PubServer(BaseHTTPRequestHandler):
cookie):
return
# edit links from the left column of the timeline in web interface
if self._editLinks(callingDomain, self.path,
self.server.translate,
self.server.baseDir,
self.server.httpPrefix,
self.server.domain,
self.server.port,
cookie):
return
# edit newswire from the right column of the timeline
if self._editNewswire(callingDomain, self.path,
self.server.translate,
self.server.baseDir,
self.server.httpPrefix,
self.server.domain,
self.server.port,
cookie):
return
if self._showNewPost(callingDomain, self.path,
self.server.mediaInstance,
self.server.translate,
@ -10049,6 +10440,26 @@ class PubServer(BaseHTTPRequestHandler):
self.server.i2pDomain, self.server.debug)
return
if authorized and self.path.endswith('/linksdata'):
self._linksUpdate(callingDomain, cookie, authorized, self.path,
self.server.baseDir, self.server.httpPrefix,
self.server.domain,
self.server.domainFull,
self.server.onionDomain,
self.server.i2pDomain, self.server.debug,
self.server.defaultTimeline)
return
if authorized and self.path.endswith('/newswiredata'):
self._newswireUpdate(callingDomain, cookie, authorized, self.path,
self.server.baseDir, self.server.httpPrefix,
self.server.domain,
self.server.domainFull,
self.server.onionDomain,
self.server.i2pDomain, self.server.debug,
self.server.defaultTimeline)
return
self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 3)
# moderator action buttons
@ -10660,6 +11071,9 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
httpd.unitTest = unitTest
httpd.YTReplacementDomain = YTReplacementDomain
# newswire storing rss feeds
httpd.newswire = {}
# This counter is used to update the list of blocked domains in memory.
# It helps to avoid touching the disk and so improves flooding resistance
httpd.blocklistUpdateCtr = 0

1905
epicyon-links.css 100644

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@
:root {
--main-bg-color: #282c37;
--column-left-color: #282c37;
--link-bg-color: #282c37;
--dropdown-fg-color: #dddddd;
--dropdown-bg-color: #111;
@ -12,6 +13,7 @@
--main-bg-color-report: #221c27;
--main-header-color-roles: #282237;
--main-fg-color: #dddddd;
--column-left-fg-color: #dddddd;
--main-link-color: #999;
--main-link-color-hover: #bbb;
--main-visited-color: #888;
@ -21,6 +23,7 @@
--font-size-header-mobile: 32px;
--font-color-header: #ccc;
--font-size-button-mobile: 34px;
--font-size-links: 18px;
--font-size: 30px;
--font-size2: 24px;
--font-size3: 38px;
@ -61,6 +64,14 @@
--quote-font-weight: normal;
--quote-font-size: 120%;
--line-spacing: 130%;
--column-left-width: 10vw;
--column-center-width: 80vw;
--column-right-width: 10vw;
--column-left-header-background: #555;
--column-left-header-color: #fff;
--column-left-header-size: 20px;
--column-left-icon-size: 20%;
--column-right-icon-size: 20%;
}
@font-face {
@ -84,9 +95,7 @@ body, html {
height: 100%;
font-family: Arial, Helvetica, sans-serif;
max-width: 80%;
min-width: 950px;
margin: 0 auto;
font-size: var(--font-size);
line-height: var(--line-spacing);
}
@ -126,6 +135,15 @@ h1 {
color: var(--title-color);
}
h3.linksHeader {
background-color: var(--column-left-header-background);
color: var(--column-left-header-color);
font-size: var(--column-left-header-size);
text-transform: uppercase;
padding: 4px;
border: none;
}
a, u {
color: var(--main-fg-color);
}
@ -158,15 +176,6 @@ a:focus {
border: 2px solid var(--focus-color);
}
.timeline-banner {
background-image: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 0.5)), url("banner.png");
height: 10%;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
position: relative;
}
.hero-image {
background-image: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 0.5)), url("image.png");
height: 50%;
@ -923,6 +932,101 @@ aside .toggle-inside li {
}
@media screen and (min-width: 400px) {
body, html {
background-color: var(--main-bg-color);
color: var(--main-fg-color);
height: 100%;
font-family: Arial, Helvetica, sans-serif;
min-width: 950px;
font-size: var(--font-size);
line-height: var(--line-spacing);
}
.timeline-banner {
background-image: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 0.5)), url("banner.png");
height: 15%;
background-position: center;
background-repeat: no-repeat;
background-size: 100vw;
position: relative;
}
.timeline {
border: 0;
width: 100vw;
}
.col-left a:link {
background: var(--column-left-color);
}
.col-left a:visited {
background: var(--column-left-color);
}
.column-left {
background-color: var(--column-left-color);
width: var(--column-left-width);
}
.col-left {
color: var(--column-left-fg-color);
padding: 10px 10px;
font-size: var(--font-size-links);
float: left;
width: var(--column-left-width);
}
.col-left img.leftColEdit {
background: var(--column-left-color);
margin: 40px 0;
width: var(--column-left-icon-size);
}
.col-left img.leftColEditImage {
background: var(--column-left-color);
width: var(--column-left-icon-size);
float: right;
}
.col-left img.leftColImg {
background: var(--column-left-color);
width: 100%;
margin: 0 0;
padding: 0 0;
}
.column-center {
width: var(--column-center-width);
}
.col-center {
width: var(--column-center-width);
background-color: var(--main-bg-color);
}
.col-right a:link {
background: var(--column-left-color);
}
.col-right a:visited {
background: var(--column-left-color);
}
.column-right {
background-color: var(--column-left-color);
width: var(--column-right-width);
}
.col-right {
background-color: var(--column-left-color);
color: var(--column-left-fg-color);
padding-right: 30px;
font-size: var(--font-size-links);
width: var(--column-right-width);
}
.col-right img.rightColEdit {
background: var(--column-left-color);
margin: 40px 0;
width: var(--column-right-icon-size);
}
.col-right img.rightColEditImage {
background: var(--column-left-color);
width: var(--column-right-icon-size);
float: right;
}
.col-right img.rightColImg {
background: var(--column-left-color);
width: 100%;
margin: 0 0;
padding: 0 0;
}
.likesCount {
font-size: var(--font-size-likes);
font-family: Arial, Helvetica, sans-serif;
@ -1372,6 +1476,53 @@ aside .toggle-inside li {
}
@media screen and (max-width: 1000px) {
body, html {
background-color: var(--main-bg-color);
color: var(--main-fg-color);
height: 100%;
font-family: Arial, Helvetica, sans-serif;
min-width: 950px;
margin-left: 0;
font-size: var(--font-size);
line-height: var(--line-spacing);
}
.timeline-banner {
background-image: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 0.5)), url("banner.png");
height: 6%;
background-position: center;
background-repeat: no-repeat;
background-size: 145vw;
position: relative;
}
.timeline {
border: 0;
width: 100vw;
}
.column-left {
display: none;
width: 0%;
}
.col-left {
float: left;
width: 0%;
display: none;
}
.col-center {
width: 100vw;
}
.col-right {
float: right;
width: 0%;
display: none;
}
.column-right {
display: none;
width: 0%;
}
.column-center {
width: 100%;
}
.likesCount {
font-size: var(--font-size-likes-mobile);
font-family: Arial, Helvetica, sans-serif;

View File

@ -28,6 +28,7 @@ from posts import getUserUrl
from posts import checkDomains
from session import createSession
from session import getJson
from newswire import getRSS
from filters import addFilter
from filters import removeFilter
import os
@ -176,6 +177,8 @@ parser.add_argument('--postsraw', dest='postsraw', type=str,
help='Show raw json of posts for the given handle')
parser.add_argument('--json', dest='json', type=str, default=None,
help='Show the json for a given activitypub url')
parser.add_argument('--rss', dest='rss', type=str, default=None,
help='Show an rss feed for a given url')
parser.add_argument('-f', '--federate', nargs='+', dest='federationList',
help='Specify federation list separated by spaces')
parser.add_argument("--repliesEnabled", "--commentsEnabled",
@ -595,6 +598,12 @@ if args.json:
pprint(testJson)
sys.exit()
if args.rss:
session = createSession(None)
testRSS = getRSS(session, args.rss)
pprint(testRSS)
sys.exit()
# create cache for actors
if not os.path.isdir(baseDir + '/cache'):
os.mkdir(baseDir + '/cache')
@ -615,11 +624,15 @@ if not args.mediainstance:
mediaInstance = getConfigParam(baseDir, 'mediaInstance')
if mediaInstance is not None:
args.mediainstance = mediaInstance
if args.mediainstance:
args.blogsinstance = False
if not args.blogsinstance:
blogsInstance = getConfigParam(baseDir, 'blogsInstance')
if blogsInstance is not None:
args.blogsinstance = blogsInstance
if args.blogsinstance:
args.mediainstance = False
# set the instance title in config.json
title = getConfigParam(baseDir, 'instanceTitle')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 29 KiB

177
newswire.py 100644
View File

@ -0,0 +1,177 @@
__filename__ = "newswire.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.1.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
import os
import requests
from socket import error as SocketError
import errno
from datetime import datetime
from collections import OrderedDict
def rss2Header(httpPrefix: str,
nickname: str, domainFull: str,
title: str, translate: {}) -> str:
rssStr = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"
rssStr += "<rss version=\"2.0\">"
rssStr += '<channel>'
if title.startswith('News'):
rssStr += ' <title>Newswire</title>'
else:
rssStr += ' <title>' + translate[title] + '</title>'
if title.startswith('News'):
rssStr += ' <link>' + httpPrefix + '://' + domainFull + \
'/newswire.xml' + '</link>'
else:
rssStr += ' <link>' + httpPrefix + '://' + domainFull + \
'/users/' + nickname + '/rss.xml' + '</link>'
return rssStr
def rss2Footer() -> str:
rssStr = '</channel>'
rssStr += '</rss>'
return rssStr
def xml2StrToDict(xmlStr: str) -> {}:
"""Converts an xml 2.0 string to a dictionary
"""
if '<item>' not in xmlStr:
return {}
result = {}
rssItems = xmlStr.split('<item>')
for rssItem in rssItems:
if '<title>' not in rssItem:
continue
if '</title>' not in rssItem:
continue
if '<link>' not in rssItem:
continue
if '</link>' not in rssItem:
continue
if '<pubDate>' not in rssItem:
continue
if '</pubDate>' not in rssItem:
continue
title = rssItem.split('<title>')[1]
title = title.split('</title>')[0]
link = rssItem.split('<link>')[1]
link = link.split('</link>')[0]
pubDate = rssItem.split('<pubDate>')[1]
pubDate = pubDate.split('</pubDate>')[0]
parsed = False
try:
publishedDate = \
datetime.strptime(pubDate, "%a, %d %b %Y %H:%M:%S %z")
result[str(publishedDate)] = [title, link]
parsed = True
except BaseException:
pass
if not parsed:
try:
publishedDate = \
datetime.strptime(pubDate, "%a, %d %b %Y %H:%M:%S UT")
result[str(publishedDate) + '+00:00'] = [title, link]
parsed = True
except BaseException:
print('WARN: unrecognized RSS date format: ' + pubDate)
pass
return result
def xmlStrToDict(xmlStr: str) -> {}:
"""Converts an xml string to a dictionary
"""
if 'rss version="2.0"' in xmlStr:
return xml2StrToDict(xmlStr)
return {}
def getRSS(session, url: str) -> {}:
"""Returns an RSS url as a dict
"""
if not isinstance(url, str):
print('url: ' + str(url))
print('ERROR: getRSS url should be a string')
return None
headers = {
'Accept': 'text/xml; charset=UTF-8'
}
params = None
sessionParams = {}
sessionHeaders = {}
if headers:
sessionHeaders = headers
if params:
sessionParams = params
sessionHeaders['User-Agent'] = \
'Mozilla/5.0 (X11; Linux x86_64; rv:81.0) Gecko/20100101 Firefox/81.0'
if not session:
print('WARN: no session specified for getRSS')
try:
result = session.get(url, headers=sessionHeaders, params=sessionParams)
return xmlStrToDict(result.text)
except requests.exceptions.RequestException as e:
print('ERROR: getRSS failed\nurl: ' + str(url) + '\n' +
'headers: ' + str(sessionHeaders) + '\n' +
'params: ' + str(sessionParams) + '\n')
print(e)
except ValueError as e:
print('ERROR: getRSS failed\nurl: ' + str(url) + '\n' +
'headers: ' + str(sessionHeaders) + '\n' +
'params: ' + str(sessionParams) + '\n')
print(e)
except SocketError as e:
if e.errno == errno.ECONNRESET:
print('WARN: connection was reset during getRSS')
print(e)
return None
def getRSSfromDict(baseDir: str, newswire: {},
httpPrefix: str, domainFull: str,
title: str, translate: {}) -> str:
"""Returns an rss feed from the current newswire dict.
This allows other instances to subscribe to the same newswire
"""
rssStr = rss2Header(httpPrefix,
None, domainFull,
'Newswire', translate)
for published, fields in newswire.items():
rssStr += '<item>\n'
rssStr += ' <title>' + fields[0] + '</title>\n'
rssStr += ' <link>' + fields[1] + '</link>\n'
pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ")
rssDateStr = pubDate.strftime("%a, %d %b %Y %H:%M:%S UT")
rssStr += ' <pubDate>' + rssDateStr + '</pubDate>\n'
rssStr += '</item>\n'
rssStr += rss2Footer()
return rssStr
def getDictFromNewswire(session, baseDir: str) -> {}:
"""Gets rss feeds as a dictionary from newswire file
"""
subscriptionsFilename = baseDir + '/accounts/newswire.txt'
if not os.path.isfile(subscriptionsFilename):
return {}
rssFeed = []
with open(subscriptionsFilename, 'r') as fp:
rssFeed = fp.readlines()
result = {}
for url in rssFeed:
url = url.strip()
if '://' not in url:
continue
if url.startswith('#'):
continue
result = dict(result.items() + getRSS(session, url).items())
sortedResult = OrderedDict(sorted(result.items(), reverse=False))
return sortedResult

View File

@ -54,7 +54,7 @@ def createSession(proxyType: str):
def getJson(session, url: str, headers: {}, params: {},
version='1.0.0', httpPrefix='https',
version='1.1.0', httpPrefix='https',
domain='testdomain') -> {}:
if not isinstance(url, str):
print('url: ' + str(url))

View File

@ -2125,7 +2125,7 @@ def testReplaceEmailQuote():
"<p>Some other text.</p>"
resultStr = htmlReplaceEmailQuote(testStr)
if resultStr != expectedStr:
print('Result: ' + resultStr)
print('Result: ' + str(resultStr))
print('Expect: ' + expectedStr)
assert resultStr == expectedStr
@ -2135,7 +2135,26 @@ def testReplaceEmailQuote():
"second line</blockquote></p><p>Some question?</p>"
resultStr = htmlReplaceEmailQuote(testStr)
if resultStr != expectedStr:
print('Result: ' + resultStr)
print('Result: ' + str(resultStr))
print('Expect: ' + expectedStr)
assert resultStr == expectedStr
testStr = "<p><span class=\"h-card\">" + \
"<a href=\"https://somedomain/@somenick\" " + \
"class=\"u-url mention\">@<span>somenick</span>" + \
"</a></span> </p><p>&gt; Text1.<br />&gt; <br />" + \
"&gt; Text2<br />&gt; <br />&gt; Text3<br />" + \
"&gt;<br />&gt; Text4<br />&gt; <br />&gt; " + \
"Text5<br />&gt; <br />&gt; Text6</p><p>Text7</p>"
expectedStr = "<p><span class=\"h-card\">" + \
"<a href=\"https://somedomain/@somenick\" " + \
"class=\"u-url mention\">@<span>somenick</span></a>" + \
"</span> </p><p><blockquote> Text1.<br /><br />" + \
"Text2<br /><br />Text3<br />&gt;<br />Text4<br />" + \
"<br />Text5<br /><br />Text6</blockquote></p><p>Text7</p>"
resultStr = htmlReplaceEmailQuote(testStr)
if resultStr != expectedStr:
print('Result: ' + str(resultStr))
print('Expect: ' + expectedStr)
assert resultStr == expectedStr

View File

@ -15,7 +15,7 @@ from shutil import copyfile
def getThemeFiles() -> []:
return ('epicyon.css', 'login.css', 'follow.css',
'suspended.css', 'calendar.css', 'blog.css',
'options.css', 'search.css')
'options.css', 'search.css', 'links.css')
def getThemesList() -> []:
@ -264,12 +264,17 @@ def setThemeIndymedia(baseDir: str):
"font-size4": "24px",
"font-size5": "22px",
"main-bg-color": "black",
"column-left-header-color": "#fff",
"column-left-header-background": "#555",
"column-left-header-size": "20px",
"column-left-color": "#003366",
"text-entry-background": "#0f0d10",
"link-bg-color": "black",
"main-link-color": "#ff9900",
"main-link-color-hover": "#d09338",
"main-visited-color": "#ffb900",
"main-fg-color": "white",
"column-left-fg-color": "white",
"main-bg-color-dm": "#0b0a0a",
"border-color": "#003366",
"border-width": "0",
@ -292,6 +297,10 @@ def setThemeIndymedia(baseDir: str):
"title-text": "white",
"title-background": "#003366",
"quote-right-margin": "0.1em",
"column-left-width": "10vw",
"column-center-width": "70vw",
"column-right-width": "20vw",
"column-right-icon-size": "11%"
}
setThemeFromDict(baseDir, name, themeParams, bgParams)
@ -311,6 +320,7 @@ def setThemeBlue(baseDir: str):
"gallery-font-size": "35px",
"gallery-font-size-mobile": "55px",
"main-bg-color": "#002365",
"column-left-color": "#002365",
"text-entry-background": "#002365",
"link-bg-color": "#002365",
"main-bg-color-reply": "#002365",
@ -348,11 +358,13 @@ def setThemeNight(baseDir: str):
"font-size4": "24px",
"font-size5": "22px",
"main-bg-color": "#0f0d10",
"column-left-color": "#0f0d10",
"text-entry-background": "#0f0d10",
"link-bg-color": "#0f0d10",
"main-link-color": "ff9900",
"main-link-color-hover": "#d09338",
"main-fg-color": "#a961ab",
"column-left-fg-color": "#a961ab",
"main-bg-color-dm": "#0b0a0a",
"border-color": "#606984",
"main-bg-color-reply": "#0f0d10",
@ -398,6 +410,7 @@ def setThemeStarlight(baseDir: str):
"font-size4": "24px",
"font-size5": "22px",
"main-bg-color": "#0f0d10",
"column-left-color": "#0f0d10",
"text-entry-background": "#0f0d10",
"link-bg-color": "#0f0d10",
"main-link-color": "#ffc4bc",
@ -405,6 +418,7 @@ def setThemeStarlight(baseDir: str):
"title-color": "#ffc4bc",
"main-visited-color": "#e1c4bc",
"main-fg-color": "#ffc4bc",
"column-left-fg-color": "#ffc4bc",
"main-bg-color-dm": "#0b0a0a",
"border-color": "#69282c",
"border-width": "3px",
@ -457,6 +471,7 @@ def setThemeHenge(baseDir: str):
"font-size4": "24px",
"font-size5": "22px",
"main-bg-color": "#383335",
"column-left-color": "#383335",
"text-entry-background": "#383335",
"link-bg-color": "#383335",
"main-link-color": "white",
@ -464,6 +479,7 @@ def setThemeHenge(baseDir: str):
"title-color": "white",
"main-visited-color": "#e1c4bc",
"main-fg-color": "white",
"column-left-fg-color": "white",
"main-bg-color-dm": "#343335",
"border-color": "#222",
"border-width": "5px",
@ -506,6 +522,7 @@ def setThemeZen(baseDir: str):
setThemeInConfig(baseDir, name)
themeParams = {
"main-bg-color": "#5c4e41",
"column-left-color": "#5c4e41",
"text-entry-background": "#5c4e41",
"link-bg-color": "#5c4e41",
"main-bg-color-reply": "#5c4e41",
@ -565,6 +582,7 @@ def setThemeLCD(baseDir: str):
name = 'lcd'
themeParams = {
"main-bg-color": "#9fb42b",
"column-left-color": "#9fb42b",
"link-bg-color": "#33390d",
"text-entry-foreground": "#33390d",
"text-entry-background": "#9fb42b",
@ -573,6 +591,7 @@ def setThemeLCD(baseDir: str):
"main-bg-color-dm": "#5fb42b",
"main-header-color-roles": "#9fb42b",
"main-fg-color": "#33390d",
"column-left-fg-color": "#33390d",
"border-color": "#33390d",
"border-width": "5px",
"main-link-color": "#9fb42b",
@ -641,11 +660,13 @@ def setThemePurple(baseDir: str):
"font-size4": "24px",
"font-size5": "22px",
"main-bg-color": "#1f152d",
"column-left-color": "#1f152d",
"link-bg-color": "#1f152d",
"main-bg-color-reply": "#1a142d",
"main-bg-color-report": "#12152d",
"main-header-color-roles": "#1f192d",
"main-fg-color": "#f98bb0",
"column-left-fg-color": "#f98bb0",
"border-color": "#3f2145",
"main-link-color": "#ff42a0",
"main-link-color-hover": "white",
@ -689,12 +710,14 @@ def setThemeHacker(baseDir: str):
themeParams = {
"focus-color": "green",
"main-bg-color": "black",
"column-left-color": "black",
"link-bg-color": "black",
"main-bg-color-dm": "#0b0a0a",
"main-bg-color-reply": "#030202",
"main-bg-color-report": "#050202",
"main-header-color-roles": "#1f192d",
"main-fg-color": "#00ff00",
"column-left-fg-color": "#00ff00",
"border-color": "#035103",
"main-link-color": "#2fff2f",
"main-link-color-hover": "#afff2f",
@ -747,6 +770,7 @@ def setThemeLight(baseDir: str):
"font-size4": "24px",
"font-size5": "22px",
"rgba(0, 0, 0, 0.5)": "rgba(0, 0, 0, 0.0)",
"column-left-color": "#e6ebf0",
"main-bg-color": "#e6ebf0",
"main-bg-color-dm": "#e3dbf0",
"link-bg-color": "#e6ebf0",
@ -754,6 +778,7 @@ def setThemeLight(baseDir: str):
"main-bg-color-report": "#e3dbf0",
"main-header-color-roles": "#ebebf0",
"main-fg-color": "#2d2c37",
"column-left-fg-color": "#2d2c37",
"border-color": "#c0cdd9",
"main-link-color": "#2a2c37",
"main-link-color-hover": "#aa2c37",
@ -804,12 +829,14 @@ def setThemeSolidaric(baseDir: str):
"font-size5": "22px",
"rgba(0, 0, 0, 0.5)": "rgba(0, 0, 0, 0.0)",
"main-bg-color": "white",
"column-left-color": "white",
"main-bg-color-dm": "white",
"link-bg-color": "white",
"main-bg-color-reply": "white",
"main-bg-color-report": "white",
"main-header-color-roles": "#ebebf0",
"main-fg-color": "#2d2c37",
"column-left-fg-color": "#2d2c37",
"border-color": "#c0cdd9",
"main-link-color": "#2a2c37",
"main-link-color-hover": "#aa2c37",
@ -864,6 +891,10 @@ def setThemeImages(baseDir: str, name: str) -> None:
baseDir + '/img/banner.png'
searchBannerFilename = \
baseDir + '/img/search_banner.png'
leftColImageFilename = \
baseDir + '/img/left_col_image.png'
rightColImageFilename = \
baseDir + '/img/right_col_image.png'
else:
profileImageFilename = \
baseDir + '/img/image_' + themeNameLower + '.png'
@ -871,6 +902,10 @@ def setThemeImages(baseDir: str, name: str) -> None:
baseDir + '/img/banner_' + themeNameLower + '.png'
searchBannerFilename = \
baseDir + '/img/search_banner_' + themeNameLower + '.png'
leftColImageFilename = \
baseDir + '/img/left_col_image_' + themeNameLower + '.png'
rightColImageFilename = \
baseDir + '/img/right_col_image_' + themeNameLower + '.png'
backgroundNames = ('login', 'shares', 'delete', 'follow',
'options', 'block', 'search', 'calendar')
@ -937,6 +972,29 @@ def setThemeImages(baseDir: str, name: str) -> None:
except BaseException:
pass
try:
if os.path.isfile(leftColImageFilename):
copyfile(leftColImageFilename,
accountDir + '/left_col_image.png')
else:
if os.path.isfile(accountDir +
'/left_col_image.png'):
os.remove(accountDir + '/left_col_image.png')
except BaseException:
pass
try:
if os.path.isfile(rightColImageFilename):
copyfile(rightColImageFilename,
accountDir + '/right_col_image.png')
else:
if os.path.isfile(accountDir +
'/right_col_image.png'):
os.remove(accountDir + '/right_col_image.png')
except BaseException:
pass
def setTheme(baseDir: str, name: str) -> bool:
result = False

View File

@ -287,5 +287,14 @@
"Autogenerated Hashtags": "علامات التجزئة المُنشأة تلقائيًا",
"Autogenerated Content Warnings": "تحذيرات المحتوى المُنشأ تلقائيًا",
"Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag محظور"
"Hashtag Blocked": "Hashtag محظور",
"This is a blogging instance": "هذا مثال على المدونات",
"Edit Links": "تحرير الارتباطات",
"One link per line. Description followed by the link.": "رابط واحد في كل سطر. الوصف متبوع بالرابط.",
"Left column image": "صورة العمود الأيسر",
"Right column image": "صورة العمود الأيمن",
"RSS feed for this site": "تغذية RSS لهذا الموقع",
"Edit newswire": "تحرير الأخبار",
"Add RSS feed links below.": "إضافة روابط تغذية RSS أدناه.",
"Newswire RSS Feed": "Newswire موجز RSS"
}

View File

@ -287,5 +287,14 @@
"Autogenerated Hashtags": "Hashtags autogenerats",
"Autogenerated Content Warnings": "Advertiments de contingut autogenerats",
"Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag bloquejat"
"Hashtag Blocked": "Hashtag bloquejat",
"This is a blogging instance": "Aquesta és una instància de blocs",
"Edit Links": "Edita els enllaços",
"One link per line. Description followed by the link.": "Un enllaç per línia. Descripció seguida de l'enllaç.",
"Left column image": "Imatge de la columna esquerra",
"Right column image": "Imatge de la columna dreta",
"RSS feed for this site": "Feed RSS per a aquest lloc",
"Edit newswire": "Editeu newswire",
"Add RSS feed links below.": "Afegiu enllaços de canals RSS a continuació.",
"Newswire RSS Feed": "Feed RSS de Newswire"
}

View File

@ -287,5 +287,14 @@
"Autogenerated Hashtags": "Hashtags awtogeneiddiedig",
"Autogenerated Content Warnings": "Rhybuddion Cynnwys Autogenerated",
"Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag wedi'i Blocio"
"Hashtag Blocked": "Hashtag wedi'i Blocio",
"This is a blogging instance": "Dyma enghraifft blogio",
"Edit Links": "Golygu Dolenni",
"One link per line. Description followed by the link.": "Un dolen y llinell. Disgrifiad wedi'i ddilyn gan y ddolen.",
"Left column image": "Delwedd colofn chwith",
"Right column image": "Delwedd colofn dde",
"RSS feed for this site": "Porthiant RSS ar gyfer y wefan hon",
"Edit newswire": "Golygu newyddion",
"Add RSS feed links below.": "Ychwanegwch ddolenni porthiant RSS isod.",
"Newswire RSS Feed": "Newswire RSS Feed"
}

View File

@ -287,5 +287,14 @@
"Autogenerated Hashtags": "Automatisch generierte Hashtags",
"Autogenerated Content Warnings": "Warnungen vor automatisch generierten Inhalten",
"Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag blockiert"
"Hashtag Blocked": "Hashtag blockiert",
"This is a blogging instance": "Dies ist eine Blogging-Instanz",
"Edit Links": "Links bearbeiten",
"One link per line. Description followed by the link.": "Ein Link pro Zeile. Beschreibung gefolgt vom Link.",
"Left column image": "Bild in der linken Spalte",
"Right column image": "Bild in der rechten Spalte",
"RSS feed for this site": "RSS-Feed für diese Site",
"Edit newswire": "Newswire bearbeiten",
"Add RSS feed links below.": "Fügen Sie unten RSS-Feed-Links hinzu.",
"Newswire RSS Feed": "Newswire RSS Feed"
}

View File

@ -287,5 +287,14 @@
"Autogenerated Hashtags": "Autogenerated Hashtags",
"Autogenerated Content Warnings": "Autogenerated Content Warnings",
"Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag Blocked"
"Hashtag Blocked": "Hashtag Blocked",
"This is a blogging instance": "This is a blogging instance",
"Edit Links": "Edit Links",
"One link per line. Description followed by the link.": "One link per line. Description followed by the link. Titles should begin with #",
"Left column image": "Left column image",
"Right column image": "Right column image",
"RSS feed for this site": "RSS feed for this site",
"Edit newswire": "Edit newswire",
"Add RSS feed links below.": "Add RSS feed links below.",
"Newswire RSS Feed": "Newswire RSS Feed"
}

View File

@ -287,5 +287,14 @@
"Autogenerated Hashtags": "Hashtags autogenerados",
"Autogenerated Content Warnings": "Advertencias de contenido generado automáticamente",
"Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag bloqueada"
"Hashtag Blocked": "Hashtag bloqueada",
"This is a blogging instance": "Esta es una instancia de blogs",
"Edit Links": "Editar enlaces",
"One link per line. Description followed by the link.": "Un enlace por línea. Descripción seguida del enlace.",
"Left column image": "Imagen de la columna izquierda",
"Right column image": "Imagen de la columna derecha",
"RSS feed for this site": "Fuente RSS para este sitio",
"Edit newswire": "Editar newswire",
"Add RSS feed links below.": "Agregue los enlaces de fuentes RSS a continuación.",
"Newswire RSS Feed": "Canal RSS de Newswire"
}

View File

@ -287,5 +287,14 @@
"Autogenerated Hashtags": "Hashtags générés automatiquement",
"Autogenerated Content Warnings": "Avertissements de contenu générés automatiquement",
"Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag bloqué"
"Hashtag Blocked": "Hashtag bloqué",
"This is a blogging instance": "Ceci est une instance de blog",
"Edit Links": "Modifier les liens",
"One link per line. Description followed by the link.": "Un lien par ligne. Description suivie du lien.",
"Left column image": "Image de la colonne de gauche",
"Right column image": "Image de la colonne de droite",
"RSS feed for this site": "Flux RSS de ce site",
"Edit newswire": "Modifier le fil d'actualité",
"Add RSS feed links below.": "Ajoutez des liens de flux RSS ci-dessous.",
"Newswire RSS Feed": "Flux RSS de Newswire"
}

View File

@ -287,5 +287,14 @@
"Autogenerated Hashtags": "Hashtags uathghinte",
"Autogenerated Content Warnings": "Rabhaidh Ábhar Uathghinte",
"Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag Blocáilte"
"Hashtag Blocked": "Hashtag Blocáilte",
"This is a blogging instance": "Seo sampla blagála",
"Edit Links": "Cuir Naisc in eagar",
"One link per line. Description followed by the link.": "Nasc amháin in aghaidh an líne. Cur síos agus an nasc ina dhiaidh sin.",
"Left column image": "Íomhá colún ar chlé",
"Right column image": "Íomhá colún ar dheis",
"RSS feed for this site": "Fotha RSS don láithreán seo",
"Edit newswire": "Cuir sreang nuachta in eagar",
"Add RSS feed links below.": "Cuir naisc beatha RSS thíos.",
"Newswire RSS Feed": "Newswire RSS Feed"
}

View File

@ -287,5 +287,14 @@
"Autogenerated Hashtags": "ऑटोजेनरेटेड हैशटैग",
"Autogenerated Content Warnings": "स्वतः प्राप्त सामग्री चेतावनी",
"Indymedia": "Indymedia",
"Hashtag Blocked": "हैशटैग अवरुद्ध"
"Hashtag Blocked": "हैशटैग अवरुद्ध",
"This is a blogging instance": "यह एक ब्लॉगिंग उदाहरण है",
"Edit Links": "लिंक संपादित करें",
"One link per line. Description followed by the link.": "प्रति पंक्ति एक लिंक। लिंक के बाद विवरण।",
"Left column image": "बाएं स्तंभ की छवि",
"Right column image": "राइट कॉलम छवि",
"RSS feed for this site": "इस साइट के लिए आरएसएस फ़ीड",
"Edit newswire": "नवांश संपादित करें",
"Add RSS feed links below.": "नीचे आरएसएस फ़ीड लिंक जोड़ें।",
"Newswire RSS Feed": "Newswire RSS फ़ीड"
}

View File

@ -287,5 +287,14 @@
"Autogenerated Hashtags": "Hashtag generati automaticamente",
"Autogenerated Content Warnings": "Avvisi sui contenuti generati automaticamente",
"Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag bloccato"
"Hashtag Blocked": "Hashtag bloccato",
"This is a blogging instance": "Questa è un'istanza di blog",
"Edit Links": "Modifica collegamenti",
"One link per line. Description followed by the link.": "Un collegamento per riga. Descrizione seguita dal collegamento.",
"Left column image": "Immagine della colonna di sinistra",
"Right column image": "Immagine della colonna di destra",
"RSS feed for this site": "Feed RSS per questo sito",
"Edit newswire": "Modifica newswire",
"Add RSS feed links below.": "Aggiungi i link ai feed RSS di seguito.",
"Newswire RSS Feed": "Feed RSS di Newswire"
}

View File

@ -287,5 +287,14 @@
"Autogenerated Hashtags": "自動生成されたハッシュタグ",
"Autogenerated Content Warnings": "自動生成されたコンテンツの警告",
"Indymedia": "Indymedia",
"Hashtag Blocked": "ハッシュタグがブロックされました"
"Hashtag Blocked": "ハッシュタグがブロックされました",
"This is a blogging instance": "これはブログのインスタンスです",
"Edit Links": "リンクの編集",
"One link per line. Description followed by the link.": "1行に1つのリンク。 説明の後にリンクが続きます。",
"Left column image": "左の列の画像",
"Right column image": "右の列の画像",
"RSS feed for this site": "このサイトのRSSフィード",
"Edit newswire": "ニュースワイヤーを編集",
"Add RSS feed links below.": "以下にRSSフィードリンクを追加します。",
"Newswire RSS Feed": "NewswireRSSフィード"
}

View File

@ -283,5 +283,14 @@
"Autogenerated Hashtags": "Autogenerated Hashtags",
"Autogenerated Content Warnings": "Autogenerated Content Warnings",
"Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag Blocked"
"Hashtag Blocked": "Hashtag Blocked",
"This is a blogging instance": "This is a blogging instance",
"Edit Links": "Edit Links",
"One link per line. Description followed by the link.": "One link per line. Description followed by the link. Titles should begin with #",
"Left column image": "Left column image",
"Right column image": "Right column image",
"RSS feed for this site": "RSS feed for this site",
"Edit newswire": "Edit newswire",
"Add RSS feed links below.": "Add RSS feed links below.",
"Newswire RSS Feed": "Newswire RSS Feed"
}

View File

@ -287,5 +287,14 @@
"Autogenerated Hashtags": "Hashtags autogeradas",
"Autogenerated Content Warnings": "Avisos de conteúdo gerado automaticamente",
"Indymedia": "Indymedia",
"Hashtag Blocked": "Hashtag bloqueada"
"Hashtag Blocked": "Hashtag bloqueada",
"This is a blogging instance": "Esta é uma instância de blog",
"Edit Links": "Editar Links",
"One link per line. Description followed by the link.": "Um link por linha. Descrição seguida pelo link.",
"Left column image": "Imagem da coluna esquerda",
"Right column image": "Imagem da coluna direita",
"RSS feed for this site": "Feed RSS para este site",
"Edit newswire": "Editar notícias",
"Add RSS feed links below.": "Adicione links de feed RSS abaixo.",
"Newswire RSS Feed": "Feed RSS da Newswire"
}

View File

@ -287,5 +287,14 @@
"Autogenerated Hashtags": "Автоматически сгенерированные хештеги",
"Autogenerated Content Warnings": "Автоматические предупреждения о содержании",
"Indymedia": "Indymedia",
"Hashtag Blocked": "Хештег заблокирован"
"Hashtag Blocked": "Хештег заблокирован",
"This is a blogging instance": "Это экземпляр блога",
"Edit Links": "Редактировать ссылки",
"One link per line. Description followed by the link.": "По одной ссылке в строке. Описание с последующей ссылкой.",
"Left column image": "Изображение в левом столбце",
"Right column image": "Изображение в правом столбце",
"RSS feed for this site": "RSS-канал для этого сайта",
"Edit newswire": "Редактировать ленту новостей",
"Add RSS feed links below.": "Добавьте ссылки на RSS-канал ниже.",
"Newswire RSS Feed": "Лента новостей RSS"
}

View File

@ -287,5 +287,14 @@
"Autogenerated Hashtags": "自动生成的标签",
"Autogenerated Content Warnings": "自动生成的内容警告",
"Indymedia": "Indymedia",
"Hashtag Blocked": "标签被阻止"
"Hashtag Blocked": "标签被阻止",
"This is a blogging instance": "这是一个博客实例",
"Edit Links": "编辑连结",
"One link per line. Description followed by the link.": "每行一个链接。 描述,然后是链接。",
"Left column image": "左栏图片",
"Right column image": "右栏图片",
"RSS feed for this site": "该站点的RSS feed",
"Edit newswire": "编辑新闻专线",
"Add RSS feed links below.": "在下面添加RSS feed链接。",
"Newswire RSS Feed": "Newswire RSS提要"
}

View File

@ -104,7 +104,7 @@ def getContentWarningButton(postID: str, translate: {},
return ' <details><summary><b>' + \
translate['SHOW MORE'] + '</b></summary>' + \
'<div id="' + postID + '">' + content + \
'</div></details>'
'</div></details>\n'
def getBlogAddress(actorJson: {}) -> str:
@ -581,7 +581,8 @@ def htmlSearchSharedItems(translate: {},
'name="searchtext" value="' + \
searchStrLower + '"><br>\n'
sharedItemsForm += \
' <center><a href="' + actor + \
' <center>\n' + \
' <a href="' + actor + \
'" type="submit" name="submitSearch">\n'
sharedItemsForm += \
' <img loading="lazy" ' + \
@ -614,7 +615,8 @@ def htmlSearchSharedItems(translate: {},
'name="searchtext" value="' + \
searchStrLower + '"><br>\n'
sharedItemsForm += \
' <center><a href="' + actor + \
' <center>\n' + \
' <a href="' + actor + \
'" type="submit" name="submitSearch">\n'
sharedItemsForm += \
' <img loading="lazy" ' + \
@ -777,13 +779,14 @@ def htmlHashtagSearch(nickname: str, domain: str, port: int,
if startIndex > 0:
# previous page link
hashtagSearchForm += \
'<center><a href="/tags/' + hashtag + '?page=' + \
' <center>\n' + \
' <a href="/tags/' + hashtag + '?page=' + \
str(pageNumber - 1) + \
'"><img loading="lazy" class="pageicon" src="/' + \
iconsDir + '/pageup.png" title="' + \
translate['Page up'] + \
'" alt="' + translate['Page up'] + \
'"></a></center>\n'
'"></a>\n </center>\n'
index = startIndex
while index <= endIndex:
postId = lines[index].strip('\n').strip('\r')
@ -832,11 +835,13 @@ def htmlHashtagSearch(nickname: str, domain: str, port: int,
if endIndex < noOfLines - 1:
# next page link
hashtagSearchForm += \
'<center><a href="/tags/' + hashtag + \
' <center>\n' + \
' <a href="/tags/' + hashtag + \
'?page=' + str(pageNumber + 1) + \
'"><img loading="lazy" class="pageicon" src="/' + iconsDir + \
'/pagedown.png" title="' + translate['Page down'] + \
'" alt="' + translate['Page down'] + '"></a></center>'
'" alt="' + translate['Page down'] + '"></a>' + \
' </center>'
hashtagSearchForm += htmlFooter()
return hashtagSearchForm
@ -1201,6 +1206,142 @@ def scheduledPostsExist(baseDir: str, nickname: str, domain: str) -> bool:
return False
def htmlEditLinks(translate: {}, baseDir: str, path: str,
domain: str, port: int, httpPrefix: str) -> str:
"""Shows the edit links screen
"""
if '/users/' not in path:
return ''
pathOriginal = path
path = path.replace('/inbox', '').replace('/outbox', '')
path = path.replace('/shares', '')
nickname = getNicknameFromActor(path)
if not nickname:
return ''
# is the user a moderator?
if not isModerator(baseDir, nickname):
return ''
cssFilename = baseDir + '/epicyon-links.css'
if os.path.isfile(baseDir + '/links.css'):
cssFilename = baseDir + '/links.css'
with open(cssFilename, 'r') as cssFile:
editCSS = cssFile.read()
if httpPrefix != 'https':
editCSS = \
editCSS.replace('https://', httpPrefix + '://')
editLinksForm = htmlHeader(cssFilename, editCSS)
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 += \
' <input type="submit" name="submitLinks" value="' + \
translate['Submit'] + '">\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:500px">' + \
linksStr + '</textarea>'
editLinksForm += \
'</div>'
editLinksForm += htmlFooter()
return editLinksForm
def htmlEditNewswire(translate: {}, baseDir: str, path: str,
domain: str, port: int, httpPrefix: str) -> str:
"""Shows the edit newswire screen
"""
if '/users/' not in path:
return ''
pathOriginal = path
path = path.replace('/inbox', '').replace('/outbox', '')
path = path.replace('/shares', '')
nickname = getNicknameFromActor(path)
if not nickname:
return ''
# is the user a moderator?
if not isModerator(baseDir, nickname):
return ''
cssFilename = baseDir + '/epicyon-links.css'
if os.path.isfile(baseDir + '/links.css'):
cssFilename = baseDir + '/links.css'
with open(cssFilename, 'r') as cssFile:
editCSS = cssFile.read()
if httpPrefix != 'https':
editCSS = \
editCSS.replace('https://', httpPrefix + '://')
editNewswireForm = htmlHeader(cssFilename, editCSS)
editNewswireForm += \
'<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 += \
' <input type="submit" name="submitNewswire" value="' + \
translate['Submit'] + '">\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:500px">' + newswireStr + '</textarea>'
editNewswireForm += \
'</div>'
editNewswireForm += htmlFooter()
return editNewswireForm
def htmlEditProfile(translate: {}, baseDir: str, path: str,
domain: str, port: int, httpPrefix: str) -> str:
"""Shows the edit profile screen
@ -1230,6 +1371,7 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
notifyLikes = ''
hideLikeButton = ''
mediaInstanceStr = ''
blogsInstanceStr = ''
displayNickname = nickname
bioStr = ''
donateUrl = ''
@ -1287,6 +1429,13 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
if mediaInstance:
if mediaInstance is True:
mediaInstanceStr = 'checked'
blogsInstanceStr = ''
blogsInstance = getConfigParam(baseDir, "blogsInstance")
if blogsInstance:
if blogsInstance is True:
blogsInstanceStr = 'checked'
mediaInstanceStr = ''
filterStr = ''
filterFilename = \
@ -1594,6 +1743,18 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
editProfileForm += 'name="search_banner"'
editProfileForm += ' accept="' + imageFormats + '">\n'
editProfileForm += ' <br><label class="labels">' + \
translate['Left column image'] + '</label>\n'
editProfileForm += ' <input type="file" id="left_col_image" '
editProfileForm += 'name="left_col_image"'
editProfileForm += ' accept="' + imageFormats + '">\n'
editProfileForm += ' <br><label class="labels">' + \
translate['Right column image'] + '</label>\n'
editProfileForm += ' <input type="file" id="right_col_image" '
editProfileForm += 'name="right_col_image"'
editProfileForm += ' accept="' + imageFormats + '">\n'
editProfileForm += ' </div>\n'
editProfileForm += ' <div class="container">\n'
editProfileForm += \
@ -1607,6 +1768,19 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
editProfileForm += \
' <input type="text" name="passwordconfirm" value="">\n'
editProfileForm += ' </div>\n'
if path.startswith('/users/' + adminNickname + '/'):
editProfileForm += ' <div class="container">\n'
editProfileForm += \
' <input type="checkbox" class="profilecheckbox" ' + \
'name="mediaInstance" ' + mediaInstanceStr + '> ' + \
translate['This is a media instance'] + '<br>\n'
editProfileForm += \
' <input type="checkbox" class="profilecheckbox" ' + \
'name="blogsInstance" ' + blogsInstanceStr + '> ' + \
translate['This is a blogging instance'] + '<br>\n'
editProfileForm += ' </div>\n'
editProfileForm += ' <div class="container">\n'
editProfileForm += \
' <input type="checkbox" class="profilecheckbox" ' + \
@ -1628,11 +1802,6 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
' <input type="checkbox" class="profilecheckbox" ' + \
'name="removeTwitter" ' + removeTwitter + '> ' + \
translate['Remove Twitter posts'] + '<br>\n'
if path.startswith('/users/' + adminNickname + '/'):
editProfileForm += \
' <input type="checkbox" class="profilecheckbox" ' + \
'name="mediaInstance" ' + mediaInstanceStr + '> ' + \
translate['This is a media instance'] + '<br>\n'
editProfileForm += \
' <input type="checkbox" class="profilecheckbox" ' + \
'name="notifyLikes" ' + notifyLikes + '> ' + \
@ -2639,6 +2808,7 @@ def htmlHeader(cssFilename: str, css: str, lang='en') -> str:
htmlStr += ' <style>\n' + css + '</style>\n'
htmlStr += ' <link rel="manifest" href="/manifest.json">\n'
htmlStr += ' <meta name="theme-color" content="grey">\n'
htmlStr += ' <title>Epicyon</title>\n'
htmlStr += ' </head>\n'
htmlStr += ' <body>\n'
return htmlStr
@ -2719,12 +2889,14 @@ def htmlProfileFollowing(translate: {}, baseDir: str, httpPrefix: str,
if authorized and pageNumber > 1:
# page up arrow
profileStr += \
'<center>\n<a href="' + actor + '/' + feedName + \
' <center>\n' + \
' <a href="' + actor + '/' + feedName + \
'?page=' + str(pageNumber - 1) + \
'"><img loading="lazy" class="pageicon" src="/' + \
iconsDir + '/pageup.png" title="' + \
translate['Page up'] + '" alt="' + \
translate['Page up'] + '"></a>\n</center>\n'
translate['Page up'] + '"></a>\n' + \
' </center>\n'
for item in followingJson['orderedItems']:
profileStr += \
@ -2737,12 +2909,14 @@ def htmlProfileFollowing(translate: {}, baseDir: str, httpPrefix: str,
if len(followingJson['orderedItems']) >= maxItemsPerPage:
# page down arrow
profileStr += \
'<center>\n<a href="' + actor + '/' + feedName + \
' <center>\n' + \
' <a href="' + actor + '/' + feedName + \
'?page=' + str(pageNumber + 1) + \
'"><img loading="lazy" class="pageicon" src="/' + \
iconsDir + '/pagedown.png" title="' + \
translate['Page down'] + '" alt="' + \
translate['Page down'] + '"></a>\n</center>\n'
translate['Page down'] + '"></a>\n' + \
' </center>\n'
return profileStr
@ -2899,11 +3073,13 @@ def htmlSharesTimeline(translate: {}, pageNumber: int, itemsPerPage: int,
if pageNumber > 1:
iconsDir = getIconsDir(baseDir)
timelineStr += \
'<center>\n<a href="' + actor + '/tlshares?page=' + \
' <center>\n' + \
' <a href="' + actor + '/tlshares?page=' + \
str(pageNumber - 1) + \
'"><img loading="lazy" class="pageicon" src="/' + \
iconsDir + '/pageup.png" title="' + translate['Page up'] + \
'" alt="' + translate['Page up'] + '"></a>\n</center>\n'
'" alt="' + translate['Page up'] + '"></a>\n' + \
' </center>\n'
for published, item in sharesJson.items():
showContactButton = False
@ -2919,11 +3095,13 @@ def htmlSharesTimeline(translate: {}, pageNumber: int, itemsPerPage: int,
if not lastPage:
iconsDir = getIconsDir(baseDir)
timelineStr += \
'<center>\n<a href="' + actor + '/tlshares?page=' + \
' <center>\n' + \
' <a href="' + actor + '/tlshares?page=' + \
str(pageNumber + 1) + \
'"><img loading="lazy" class="pageicon" src="/' + \
iconsDir + '/pagedown.png" title="' + translate['Page down'] + \
'" alt="' + translate['Page down'] + '"></a>\n</center>\n'
'" alt="' + translate['Page down'] + '"></a>\n' + \
' </center>\n'
return timelineStr
@ -4130,7 +4308,7 @@ def individualPostAsHtml(allowDownloads: bool,
avatarLink = ' <a class="imageAnchor" href="' + postActor + '">'
avatarLink += \
' <img loading="lazy" src="' + avatarUrl + '" title="' + \
translate['Show profile'] + '" alt=" "' + avatarPosition + '/></a>'
translate['Show profile'] + '" alt=" "' + avatarPosition + '/></a>\n'
if showAvatarOptions and \
fullDomain + '/users/' + nickname not in postActor:
@ -4280,18 +4458,21 @@ def individualPostAsHtml(allowDownloads: bool,
else:
if isDM(postJsonObject):
replyStr += \
' ' + \
'<a class="imageAnchor" href="/users/' + nickname + \
'?replydm=' + replyToLink + \
'?actor=' + postJsonObject['actor'] + \
'" title="' + translate['Reply to this post'] + '">\n'
else:
replyStr += \
' ' + \
'<a class="imageAnchor" href="/users/' + nickname + \
'?replyfollowers=' + replyToLink + \
'?actor=' + postJsonObject['actor'] + \
'" title="' + translate['Reply to this post'] + '">\n'
replyStr += \
' ' + \
'<img loading="lazy" title="' + \
translate['Reply to this post'] + '" alt="' + \
translate['Reply to this post'] + \
@ -4317,6 +4498,7 @@ def individualPostAsHtml(allowDownloads: bool,
if isBlogPost(postJsonObject):
blogPostId = postJsonObject['object']['id']
editStr += \
' ' + \
'<a class="imageAnchor" href="/users/' + nickname + \
'/tlblogs?editblogpost=' + \
blogPostId.split('/statuses/')[1] + \
@ -4329,6 +4511,7 @@ def individualPostAsHtml(allowDownloads: bool,
elif isEvent:
eventPostId = postJsonObject['object']['id']
editStr += \
' ' + \
'<a class="imageAnchor" href="/users/' + nickname + \
'/tlblogs?editeventpost=' + \
eventPostId.split('/statuses/')[1] + \
@ -4360,6 +4543,7 @@ def individualPostAsHtml(allowDownloads: bool,
'?bm=' + timelinePostBookmark + \
'?tl=' + boxName + '" title="' + announceTitle + '">\n'
announceStr += \
' ' + \
'<img loading="lazy" title="' + translate['Repeat this post'] + \
'" alt="' + translate['Repeat this post'] + \
' |" src="/' + iconsDir + '/' + announceIcon + '"/></a>\n'
@ -4425,6 +4609,7 @@ def individualPostAsHtml(allowDownloads: bool,
'?tl=' + boxName + '" title="' + \
likeTitle + likeCountStr + '">\n'
likeStr += \
' ' + \
'<img loading="lazy" title="' + likeTitle + likeCountStr + \
'" alt="' + likeTitle + \
' |" src="/' + iconsDir + '/' + likeIcon + '"/></a>\n'
@ -4457,6 +4642,7 @@ def individualPostAsHtml(allowDownloads: bool,
'?bm=' + timelinePostBookmark + \
'?tl=' + boxName + '" title="' + bookmarkTitle + '">\n'
bookmarkStr += \
' ' + \
'<img loading="lazy" title="' + bookmarkTitle + '" alt="' + \
bookmarkTitle + ' |" src="/' + iconsDir + \
'/' + bookmarkIcon + '"/></a>\n'
@ -4486,6 +4672,7 @@ def individualPostAsHtml(allowDownloads: bool,
'?delete=' + messageId + pageNumberParam + \
'" title="' + translate['Delete this post'] + '">\n'
deleteStr += \
' ' + \
'<img loading="lazy" alt="' + translate['Delete this post'] + \
' |" title="' + translate['Delete this post'] + \
'" src="/' + iconsDir + '/delete.png"/></a>\n'
@ -4497,6 +4684,7 @@ def individualPostAsHtml(allowDownloads: bool,
'?bm=' + timelinePostBookmark + \
'" title="' + translate['Mute this post'] + '">\n'
muteStr += \
' ' + \
'<img loading="lazy" alt="' + \
translate['Mute this post'] + \
' |" title="' + translate['Mute this post'] + \
@ -4509,6 +4697,7 @@ def individualPostAsHtml(allowDownloads: bool,
timelinePostBookmark + '" title="' + \
translate['Undo mute'] + '">\n'
muteStr += \
' ' + \
'<img loading="lazy" alt="' + translate['Undo mute'] + \
' |" title="' + translate['Undo mute'] + \
'" src="/' + iconsDir+'/unmute.png"/></a>\n'
@ -4574,11 +4763,13 @@ def individualPostAsHtml(allowDownloads: bool,
' 13.3.1 = ' + str(timeDiff))
titleStr += \
' ' + \
'<img loading="lazy" title="' + \
translate['announces'] + '" alt="' + \
translate['announces'] + '" src="/' + \
iconsDir + '/repeat_inactive.png" ' + \
'class="announceOrReply"/> <a href="' + \
'class="announceOrReply"/>\n' + \
' <a href="' + \
postJsonObject['object']['id'] + '">' + \
announceDisplayName + '</a>\n'
# show avatar of person replied to
@ -4599,6 +4790,7 @@ def individualPostAsHtml(allowDownloads: bool,
if announceAvatarUrl:
idx = 'Show options for this person'
replyAvatarImageInPost = \
' ' \
'<div class="timeline-avatar-reply">\n' \
' <a class="imageAnchor" ' + \
'href="/users/' + nickname + \
@ -4618,7 +4810,8 @@ def individualPostAsHtml(allowDownloads: bool,
'" alt="' + translate['announces'] + \
'" src="/' + iconsDir + \
'/repeat_inactive.png" ' + \
'class="announceOrReply"/> <a href="' + \
'class="announceOrReply"/>\n' + \
' <a href="' + \
postJsonObject['object']['id'] + '">@' + \
announceNickname + '@' + \
announceDomain + '</a>\n'
@ -4628,16 +4821,19 @@ def individualPostAsHtml(allowDownloads: bool,
translate['announces'] + '" alt="' + \
translate['announces'] + '" src="/' + iconsDir + \
'/repeat_inactive.png" ' + \
'class="announceOrReply"/> <a href="' + \
'class="announceOrReply"/>\n' + \
' <a href="' + \
postJsonObject['object']['id'] + \
'">@unattributed</a>\n'
else:
titleStr += \
' ' + \
'<img loading="lazy" title="' + translate['announces'] + \
'" alt="' + translate['announces'] + \
'" src="/' + iconsDir + \
'/repeat_inactive.png" ' + \
'class="announceOrReply"/> <a href="' + \
'class="announceOrReply"/>\n' + \
' <a href="' + \
postJsonObject['object']['id'] + '">@unattributed</a>\n'
else:
if postJsonObject['object'].get('inReplyTo'):
@ -4694,13 +4890,15 @@ def individualPostAsHtml(allowDownloads: bool,
boxName + ' 13.6 = ' +
str(timeDiff))
titleStr += \
' ' + \
'<img loading="lazy" title="' + \
translate['replying to'] + \
'" alt="' + \
translate['replying to'] + \
'" src="/' + \
iconsDir + '/reply.png" ' + \
'class="announceOrReply"/> ' + \
'class="announceOrReply"/>\n' + \
' ' + \
'<a href="' + inReplyTo + \
'">' + replyDisplayName + '</a>\n'
@ -4732,6 +4930,7 @@ def individualPostAsHtml(allowDownloads: bool,
' <div class=' + \
'"timeline-avatar-reply">\n'
replyAvatarImageInPost += \
' ' + \
'<a class="imageAnchor" ' + \
'href="/users/' + nickname + \
'?options=' + replyActor + \
@ -4739,6 +4938,7 @@ def individualPostAsHtml(allowDownloads: bool,
replyAvatarUrl + \
messageIdStr + '">\n'
replyAvatarImageInPost += \
' ' + \
'<img loading="lazy" src="' + \
replyAvatarUrl + '" '
replyAvatarImageInPost += \
@ -4746,18 +4946,20 @@ def individualPostAsHtml(allowDownloads: bool,
translate['Show profile']
replyAvatarImageInPost += \
'" alt=" "' + \
avatarPosition + '/></a>\n</div>\n'
avatarPosition + '/></a>\n' + \
' </div>\n'
else:
inReplyTo = \
postJsonObject['object']['inReplyTo']
titleStr += \
' ' + \
'<img loading="lazy" title="' + \
translate['replying to'] + \
'" alt="' + \
translate['replying to'] + \
'" src="/' + \
iconsDir + '/reply.png" ' + \
'class="announceOrReply"/> ' + \
'class="announceOrReply"/>\n' + \
' <a href="' + \
inReplyTo + '">@' + \
replyNickname + '@' + \
@ -4770,7 +4972,7 @@ def individualPostAsHtml(allowDownloads: bool,
translate['replying to'] + \
'" src="/' + \
iconsDir + \
'/reply.png" class="announceOrReply"/> ' + \
'/reply.png" class="announceOrReply"/>\n' + \
' <a href="' + \
postJsonObject['object']['inReplyTo'] + \
'">@unknown</a>\n'
@ -4789,7 +4991,8 @@ def individualPostAsHtml(allowDownloads: bool,
'" alt="' + translate['replying to'] + \
'" src="/' + \
iconsDir + '/reply.png" ' + \
'class="announceOrReply"/> <a href="' + \
'class="announceOrReply"/>\n' + \
' <a href="' + \
postJsonObject['object']['inReplyTo'] + \
'">' + postDomain + '</a>\n'
@ -4849,12 +5052,12 @@ def individualPostAsHtml(allowDownloads: bool,
containerClass = 'container dm'
if showIcons:
footerStr = '<div class="' + containerClassIcons + '">'
footerStr = '\n <div class="' + containerClassIcons + '">\n'
footerStr += replyStr + announceStr + likeStr + bookmarkStr + \
deleteStr + muteStr + editStr
footerStr += ' <a href="' + publishedLink + '" class="' + \
timeClass + '">' + publishedStr + '</a>\n'
footerStr += '</div>'
footerStr += ' </div>\n'
postIsSensitive = False
if postJsonObject['object'].get('sensitive'):
@ -4948,7 +5151,9 @@ def individualPostAsHtml(allowDownloads: bool,
contentStr = ''
else:
if not isPatch:
contentStr = '<div class="message">' + contentStr + '</div>\n'
contentStr = ' <div class="message">' + \
contentStr + \
' </div>\n'
else:
contentStr = \
'<div class="gitpatch"><pre><code>' + contentStr + \
@ -4959,8 +5164,9 @@ def individualPostAsHtml(allowDownloads: bool,
postHtml = ' <div id="' + timelinePostBookmark + \
'" class="' + containerClass + '">\n'
postHtml += avatarImageInPost
postHtml += '<p class="post-title">' + titleStr + \
replyAvatarImageInPost + '</p>\n'
postHtml += ' <div class="post-title">\n' + \
' ' + titleStr + \
replyAvatarImageInPost + ' </div>\n'
postHtml += contentStr + footerStr + '\n'
postHtml += ' </div>\n'
else:
@ -5019,6 +5225,184 @@ def htmlHighlightLabel(label: str, highlight: bool) -> str:
return '*' + label + '*'
def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str,
httpPrefix: str, translate: {},
iconsDir: str, moderator: bool) -> str:
"""Returns html content for the left column
"""
htmlStr = ''
domain = domainFull
if ':' in domain:
domain = domain.split(':')
leftColumnImageFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + \
'/left_col_image.png'
if not os.path.isfile(leftColumnImageFilename):
theme = getConfigParam(baseDir, 'theme').lower()
if theme == 'default':
theme = ''
else:
theme = '_' + theme
themeLeftColumnImageFilename = \
baseDir + '/img/left_col_image' + theme + '.png'
if os.path.isfile(themeLeftColumnImageFilename):
copyfile(themeLeftColumnImageFilename, leftColumnImageFilename)
# 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 + '/left_col_image.png" />\n' + \
' </center>\n'
if editImageClass == 'leftColEdit':
htmlStr += '\n <center>\n'
if moderator:
# 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
htmlStr += \
' <a href="' + \
httpPrefix + '://' + domainFull + \
'/blog/' + nickname + '/rss.xml">' + \
'<img class="' + editImageClass + \
'" loading="lazy" alt="' + \
translate['RSS feed for this site'] + \
'" title="' + translate['RSS feed for this site'] + \
'" src="/' + iconsDir + '/rss.png" /></a>\n'
if editImageClass == 'leftColEdit':
htmlStr += ' </center>\n'
else:
htmlStr += ' <br>\n'
linksFilename = baseDir + '/accounts/links.txt'
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'
else:
if lineStr.startswith('#') or lineStr.startswith('*'):
lineStr = lineStr[1:].strip()
htmlStr += \
' <h3 class="linksHeader">' + \
lineStr + '</h3>\n'
else:
htmlStr += \
' <p>' + lineStr + '</p>\n'
return htmlStr
def getRightColumnContent(baseDir: str, nickname: str, domainFull: str,
httpPrefix: str, translate: {},
iconsDir: str, moderator: bool) -> str:
"""Returns html content for the right column
"""
htmlStr = ''
domain = domainFull
if ':' in domain:
domain = domain.split(':')
rightColumnImageFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + \
'/right_col_image.png'
if not os.path.isfile(rightColumnImageFilename):
theme = getConfigParam(baseDir, 'theme').lower()
if theme == 'default':
theme = ''
else:
theme = '_' + theme
themeRightColumnImageFilename = \
baseDir + '/img/right_col_image' + theme + '.png'
if os.path.isfile(themeRightColumnImageFilename):
copyfile(themeRightColumnImageFilename, rightColumnImageFilename)
# 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 + '/right_col_image.png" />\n' + \
' </center>\n'
if editImageClass == 'rightColEdit':
htmlStr += '\n <center>\n'
if moderator:
# 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'
htmlStr += \
' <a href="/newswire.xml">' + \
'<img class="' + editImageClass + \
'" loading="lazy" alt="' + \
translate['Newswire RSS Feed'] + '" title="' + \
translate['Newswire RSS Feed'] + '" src="/' + \
iconsDir + '/rss.png" /></a>\n'
if editImageClass == 'rightColEdit':
htmlStr += ' </center>\n'
else:
htmlStr += ' <br>\n'
return htmlStr
def htmlTimeline(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int,
@ -5300,8 +5684,8 @@ def htmlTimeline(defaultTimeline: str,
# This creates a link to the profile page when viewed
# in lynx, but should be invisible in a graphical web browser
tlStr += \
'<a href="/users/' + nickname + '"><label class="transparent">' + \
translate['Switch to profile view'] + '</label></a>\n'
'<label class="transparent"><a href="/users/' + nickname + '">' + \
translate['Switch to profile view'] + '</a></label>\n'
# banner and row of buttons
tlStr += \
@ -5310,8 +5694,34 @@ def htmlTimeline(defaultTimeline: str,
translate['Switch to profile view'] + '">\n'
tlStr += '<div class="timeline-banner">'
tlStr += '</div>\n</a>\n'
tlStr += '<div class="container">\n'
# start the timeline
tlStr += '<table class="timeline">\n'
tlStr += ' <colgroup>\n'
tlStr += ' <col span="1" class="column-left">\n'
tlStr += ' <col span="1" class="column-center">\n'
tlStr += ' <col span="1" class="column-right">\n'
tlStr += ' </colgroup>\n'
tlStr += ' <tbody>\n'
tlStr += ' <tr>\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,
moderator)
tlStr += ' <td valign="top" class="col-left">' + \
leftColumnStr + ' </td>\n'
# center column containing posts
tlStr += ' <td valign="top" class="col-center">\n'
# start of the button header with inbox, outbox, etc
tlStr += ' <div class="container">\n'
# first button
if defaultTimeline == 'tlmedia':
tlStr += \
@ -5388,6 +5798,32 @@ def htmlTimeline(defaultTimeline: str,
sharesButtonStr + bookmarksButtonStr + eventsButtonStr + \
moderationButtonStr + newPostButtonStr
# show todays events buttons on the first inbox page
if boxName == 'inbox' and pageNumber == 1:
if todaysEventsCheck(baseDir, nickname, domain):
now = datetime.now()
# happening today button
tlStr += \
' <a href="' + usersPath + '/calendar?year=' + \
str(now.year) + '?month=' + str(now.month) + \
'?day=' + str(now.day) + '"><button class="buttonevent">' + \
translate['Happening Today'] + '</button></a>\n'
# happening this week button
if thisWeeksEventsCheck(baseDir, nickname, domain):
tlStr += \
' <a href="' + usersPath + \
'/calendar"><button class="buttonevent">' + \
translate['Happening This Week'] + '</button></a>\n'
else:
# happening this week button
if thisWeeksEventsCheck(baseDir, nickname, domain):
tlStr += \
' <a href="' + usersPath + \
'/calendar"><button class="buttonevent">' + \
translate['Happening This Week'] + '</button></a>\n'
# the search button
tlStr += \
' <a class="imageAnchor" href="' + usersPath + \
@ -5420,7 +5856,8 @@ def htmlTimeline(defaultTimeline: str,
'" alt="| ' + translate['Show/Hide Buttons'] + \
'" class="timelineicon"/></a>\n'
tlStr += followApprovals
tlStr += '</div>'
# end of the button header with inbox, outbox, etc
tlStr += ' </div>\n'
# second row of buttons for moderator actions
if moderator and boxName == 'moderation':
@ -5479,34 +5916,6 @@ def htmlTimeline(defaultTimeline: str,
if timeDiff > 100:
print('TIMELINE TIMING ' + boxName + ' 7 = ' + str(timeDiff))
# show todays events buttons on the first inbox page
if boxName == 'inbox' and pageNumber == 1:
if todaysEventsCheck(baseDir, nickname, domain):
now = datetime.now()
# happening today button
tlStr += \
'<center>\n<a href="' + usersPath + '/calendar?year=' + \
str(now.year) + '?month=' + str(now.month) + \
'?day=' + str(now.day) + '"><button class="buttonevent">' + \
translate['Happening Today'] + '</button></a>\n'
# happening this week button
if thisWeeksEventsCheck(baseDir, nickname, domain):
tlStr += \
'<a href="' + usersPath + \
'/calendar"><button class="buttonevent">' + \
translate['Happening This Week'] + '</button></a>\n'
tlStr += '</center>\n'
else:
# happening this week button
if thisWeeksEventsCheck(baseDir, nickname, domain):
tlStr += \
'<center>\n<a href="' + usersPath + \
'/calendar"><button class="buttonevent">' + \
translate['Happening This Week'] + '</button></a>\n' + \
'</center>\n'
# benchmark 8
timeDiff = int((time.time() - timelineStartTime) * 1000)
if timeDiff > 100:
@ -5515,12 +5924,14 @@ def htmlTimeline(defaultTimeline: str,
# page up arrow
if pageNumber > 1:
tlStr += \
'<center>\n<a href="' + usersPath + '/' + boxName + \
' <center>\n' + \
' <a href="' + usersPath + '/' + boxName + \
'?page=' + str(pageNumber - 1) + \
'"><img loading="lazy" class="pageicon" src="/' + \
iconsDir + '/pageup.png" title="' + \
translate['Page up'] + '" alt="' + \
translate['Page up'] + '"></a>\n</center>\n'
translate['Page up'] + '"></a>\n' + \
' </center>\n'
# show the posts
itemCtr = 0
@ -5604,6 +6015,17 @@ def htmlTimeline(defaultTimeline: str,
if boxName == 'tlmedia':
tlStr += '</div>\n'
# end of column-center
tlStr += ' </td>\n'
# right column
rightColumnStr = getRightColumnContent(baseDir, nickname, domainFull,
httpPrefix, translate, iconsDir,
moderator)
tlStr += ' <td valign="top" class="col-right">' + \
rightColumnStr + ' </td>\n'
tlStr += ' </tr>\n'
# benchmark 9
timeDiff = int((time.time() - timelineStartTime) * 1000)
if timeDiff > 100:
@ -5612,12 +6034,23 @@ def htmlTimeline(defaultTimeline: str,
# page down arrow
if itemCtr > 2:
tlStr += \
'<center>\n<a href="' + usersPath + '/' + boxName + '?page=' + \
' <tr>\n' + \
' <td class="col-left"></td>\n' + \
' <td class="col-center">\n' + \
' <center>\n' + \
' <a href="' + usersPath + '/' + boxName + '?page=' + \
str(pageNumber + 1) + \
'"><img loading="lazy" class="pageicon" src="/' + \
iconsDir + '/pagedown.png" title="' + \
translate['Page down'] + '" alt="' + \
translate['Page down'] + '"></a>\n</center>\n'
translate['Page down'] + '"></a>\n' + \
' </center>\n' + \
' </td>\n' + \
' <td class="col-right"></td>\n' + \
' </tr>\n'
tlStr += ' </tbody>\n'
tlStr += '</table>\n'
tlStr += htmlFooter()
return tlStr