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

main
Bob Mottram 2020-10-16 19:31:57 +01:00
commit 5d9a52c4f8
98 changed files with 898 additions and 309 deletions

21
blog.py
View File

@ -365,7 +365,7 @@ def htmlBlogPost(authorized: bool,
blogStr += '<img style="width:3%;min-width:50px" ' + \
'loading="lazy" alt="RSS 2.0" ' + \
'title="RSS 2.0" src="/' + \
iconsDir + '/rss.png" /></a>'
iconsDir + '/logorss.png" /></a>'
# blogStr += '<a href="' + httpPrefix + '://' + \
# domainFull + '/blog/' + nickname + '/rss.txt">'
@ -461,7 +461,7 @@ def htmlBlogPage(authorized: bool, session,
domainFull + '/blog/' + nickname + '/rss.xml">'
blogStr += '<img loading="lazy" alt="RSS 2.0" ' + \
'title="RSS 2.0" src="/' + \
iconsDir + '/rss.png" /></a>'
iconsDir + '/logorss.png" /></a>'
# blogStr += '<a href="' + httpPrefix + '://' + \
# domainFull + '/blog/' + nickname + '/rss.txt">'
@ -478,7 +478,8 @@ def htmlBlogPage(authorized: bool, session,
def htmlBlogPageRSS2(authorized: bool, session,
baseDir: str, httpPrefix: str, translate: {},
nickname: str, domain: str, port: int,
noOfItems: int, pageNumber: int) -> str:
noOfItems: int, pageNumber: int,
includeHeader: bool) -> str:
"""Returns an RSS version 2 feed containing posts
"""
if ' ' in nickname or '@' in nickname or \
@ -490,12 +491,18 @@ def htmlBlogPageRSS2(authorized: bool, session,
if port != 80 and port != 443:
domainFull = domain + ':' + str(port)
blogRSS2 = rss2Header(httpPrefix, nickname, domainFull, 'Blog', translate)
blogRSS2 = ''
if includeHeader:
blogRSS2 = rss2Header(httpPrefix, nickname, domainFull,
'Blog', translate)
blogsIndex = baseDir + '/accounts/' + \
nickname + '@' + domain + '/tlblogs.index'
if not os.path.isfile(blogsIndex):
if includeHeader:
return blogRSS2 + rss2Footer()
else:
return blogRSS2
timelineJson = createBlogsTimeline(session, baseDir,
nickname, domain, port,
@ -504,7 +511,10 @@ def htmlBlogPageRSS2(authorized: bool, session,
pageNumber)
if not timelineJson:
if includeHeader:
return blogRSS2 + rss2Footer()
else:
return blogRSS2
if pageNumber is not None:
for item in timelineJson['orderedItems']:
@ -518,7 +528,10 @@ def htmlBlogPageRSS2(authorized: bool, session,
domainFull, item,
None, True)
if includeHeader:
return blogRSS2 + rss2Footer()
else:
return blogRSS2
def htmlBlogPageRSS3(authorized: bool, session,

440
daemon.py
View File

@ -164,6 +164,8 @@ from shares import getSharesFeedForPerson
from shares import addShare
from shares import removeShare
from shares import expireShares
from utils import containsInvalidChars
from utils import isSystemAccount
from utils import setConfigParam
from utils import getConfigParam
from utils import removeIdEnding
@ -194,6 +196,7 @@ from media import removeMetaData
from cache import storePersonInCache
from cache import getPersonFromCache
from httpsig import verifyPostHeaders
from theme import setNewsAvatar
from theme import setTheme
from theme import getTheme
from theme import enableGrayscale
@ -212,6 +215,8 @@ from devices import E2EEdevicesCollection
from devices import E2EEvalidDevice
from devices import E2EEaddDevice
from newswire import getRSSfromDict
from newswire import rss2Header
from newswire import rss2Footer
from newsdaemon import runNewswireWatchdog
from newsdaemon import runNewswireDaemon
import os
@ -300,11 +305,11 @@ class PubServer(BaseHTTPRequestHandler):
accountDir = self.server.baseDir + '/accounts/' + \
nickname + '@' + self.server.domain
if not os.path.isdir(accountDir):
return False
minimalFilename = accountDir + '/minimal'
if os.path.isfile(minimalFilename):
return True
minimalFilename = accountDir + '/.notminimal'
if os.path.isfile(minimalFilename):
return False
return True
def _setMinimal(self, nickname: str, minimal: bool) -> None:
"""Sets whether an account should display minimal buttons
@ -313,11 +318,11 @@ class PubServer(BaseHTTPRequestHandler):
nickname + '@' + self.server.domain
if not os.path.isdir(accountDir):
return
minimalFilename = accountDir + '/minimal'
minimalFilename = accountDir + '/.notminimal'
minimalFileExists = os.path.isfile(minimalFilename)
if not minimal and minimalFileExists:
if minimal and minimalFileExists:
os.remove(minimalFilename)
elif minimal and not minimalFileExists:
elif not minimal and not minimalFileExists:
with open(minimalFilename, 'w+') as fp:
fp.write('\n')
@ -521,6 +526,21 @@ class PubServer(BaseHTTPRequestHandler):
self.send_header('X-Robots-Tag', 'noindex')
self.end_headers()
def _logout_redirect(self, redirect: str, cookie: str,
callingDomain: str) -> None:
if '://' not in redirect:
print('REDIRECT ERROR: redirect is not an absolute url ' +
redirect)
self.send_response(303)
self.send_header('Set-Cookie', 'epicyon=; SameSite=Strict')
self.send_header('Location', redirect)
self.send_header('Host', callingDomain)
self.send_header('InstanceID', self.server.instanceId)
self.send_header('Content-Length', '0')
self.send_header('X-Robots-Tag', 'noindex')
self.end_headers()
def _set_headers_base(self, fileFormat: str, length: int, cookie: str,
callingDomain: str) -> None:
self.send_response(200)
@ -1240,8 +1260,9 @@ class PubServer(BaseHTTPRequestHandler):
loginNickname, loginPassword, register = \
htmlGetLoginCredentials(loginParams, self.server.lastLoginTime)
if loginNickname:
if loginNickname == 'news' or loginNickname == 'inbox':
print('Invalid username login: ' + loginNickname)
if isSystemAccount(loginNickname):
print('Invalid username login: ' + loginNickname +
' (system account)')
self._clearLoginDetails(loginNickname, callingDomain)
self.server.POSTbusy = False
return
@ -1625,7 +1646,8 @@ class PubServer(BaseHTTPRequestHandler):
if debug:
print('You cannot perform an option action on yourself')
# view button on person option screen
# person options screen, view button
# See htmlPersonOptions
if '&submitView=' in optionsConfirmParams:
if debug:
print('Viewing ' + optionsActor)
@ -1634,7 +1656,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.POSTbusy = False
return
# petname submit button on person option screen
# person options screen, petname submit button
# See htmlPersonOptions
if '&submitPetname=' in optionsConfirmParams and petname:
if debug:
print('Change petname to ' + petname)
@ -1650,7 +1673,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.POSTbusy = False
return
# person notes submit button on person option screen
# person options screen, person notes submit button
# See htmlPersonOptions
if '&submitPersonNotes=' in optionsConfirmParams:
if debug:
print('Change person notes')
@ -1668,7 +1692,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.POSTbusy = False
return
# person on calendar checkbox on person option screen
# person options screen, on calendar checkbox
# See htmlPersonOptions
if '&submitOnCalendar=' in optionsConfirmParams:
onCalendar = None
if 'onCalendar=' in optionsConfirmParams:
@ -1694,7 +1719,35 @@ class PubServer(BaseHTTPRequestHandler):
self.server.POSTbusy = False
return
# block person button on person option screen
# person options screen, permission to post to newswire
# See htmlPersonOptions
if '&submitPostToNews=' in optionsConfirmParams:
if isModerator(self.server.baseDir, chooserNickname):
postsToNews = None
if 'postsToNews=' in optionsConfirmParams:
postsToNews = optionsConfirmParams.split('postsToNews=')[1]
if '&' in postsToNews:
postsToNews = postsToNews.split('&')[0]
newswireBlockedFilename = \
self.server.baseDir + '/accounts/' + \
optionsNickname + '@' + optionsDomain + '/.nonewswire'
if postsToNews == 'on':
if os.path.isfile(newswireBlockedFilename):
os.remove(newswireBlockedFilename)
else:
noNewswireFile = open(newswireBlockedFilename, "w+")
if noNewswireFile:
noNewswireFile.write('\n')
noNewswireFile.close()
self._redirect_headers(usersPath + '/' +
self.server.defaultTimeline +
'?page='+str(pageNumber), cookie,
callingDomain)
self.server.POSTbusy = False
return
# person options screen, block button
# See htmlPersonOptions
if '&submitBlock=' in optionsConfirmParams:
if debug:
print('Adding block by ' + chooserNickname +
@ -1703,7 +1756,8 @@ class PubServer(BaseHTTPRequestHandler):
domain,
optionsNickname, optionsDomainFull)
# unblock button on person option screen
# person options screen, unblock button
# See htmlPersonOptions
if '&submitUnblock=' in optionsConfirmParams:
if debug:
print('Unblocking ' + optionsActor)
@ -1719,7 +1773,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.POSTbusy = False
return
# follow button on person option screen
# person options screen, follow button
# See htmlPersonOptions
if '&submitFollow=' in optionsConfirmParams:
if debug:
print('Following ' + optionsActor)
@ -1735,7 +1790,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.POSTbusy = False
return
# unfollow button on person option screen
# person options screen, unfollow button
# See htmlPersonOptions
if '&submitUnfollow=' in optionsConfirmParams:
if debug:
print('Unfollowing ' + optionsActor)
@ -1751,7 +1807,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.POSTbusy = False
return
# DM button on person option screen
# person options screen, DM button
# See htmlPersonOptions
if '&submitDM=' in optionsConfirmParams:
if debug:
print('Sending DM to ' + optionsActor)
@ -1771,7 +1828,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.POSTbusy = False
return
# snooze button on person option screen
# person options screen, snooze button
# See htmlPersonOptions
if '&submitSnooze=' in optionsConfirmParams:
usersPath = path.split('/personoptions')[0]
thisActor = httpPrefix + '://' + domainFull + usersPath
@ -1792,7 +1850,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.POSTbusy = False
return
# unsnooze button on person option screen
# person options screen, unsnooze button
# See htmlPersonOptions
if '&submitUnSnooze=' in optionsConfirmParams:
usersPath = path.split('/personoptions')[0]
thisActor = httpPrefix + '://' + domainFull + usersPath
@ -1813,7 +1872,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.POSTbusy = False
return
# report button on person option screen
# person options screen, report button
# See htmlPersonOptions
if '&submitReport=' in optionsConfirmParams:
if debug:
print('Reporting ' + optionsActor)
@ -3309,10 +3369,95 @@ class PubServer(BaseHTTPRequestHandler):
if fields['displayNickname'] != actorJson['name']:
actorJson['name'] = fields['displayNickname']
actorChanged = True
# change media instance status
if fields.get('mediaInstance'):
self.server.mediaInstance = False
self.server.defaultTimeline = 'inbox'
if fields['mediaInstance'] == 'on':
self.server.mediaInstance = True
self.server.blogsInstance = False
self.server.newsInstance = False
self.server.defaultTimeline = 'tlmedia'
setConfigParam(baseDir,
"mediaInstance",
self.server.mediaInstance)
setConfigParam(baseDir,
"blogsInstance",
self.server.blogsInstance)
setConfigParam(baseDir,
"newsInstance",
self.server.newsInstance)
else:
if self.server.mediaInstance:
self.server.mediaInstance = False
self.server.defaultTimeline = 'inbox'
setConfigParam(baseDir,
"mediaInstance",
self.server.mediaInstance)
# change news instance status
if fields.get('newsInstance'):
self.server.newsInstance = False
self.server.defaultTimeline = 'inbox'
if fields['newsInstance'] == 'on':
self.server.newsInstance = True
self.server.blogsInstance = False
self.server.mediaInstance = False
self.server.defaultTimeline = 'tlnews'
setConfigParam(baseDir,
"mediaInstance",
self.server.mediaInstance)
setConfigParam(baseDir,
"blogsInstance",
self.server.blogsInstance)
setConfigParam(baseDir,
"newsInstance",
self.server.newsInstance)
else:
if self.server.newsInstance:
self.server.newsInstance = False
self.server.defaultTimeline = 'inbox'
setConfigParam(baseDir,
"newsInstance",
self.server.mediaInstance)
# change blog instance status
if fields.get('blogsInstance'):
self.server.blogsInstance = False
self.server.defaultTimeline = 'inbox'
if fields['blogsInstance'] == 'on':
self.server.blogsInstance = True
self.server.mediaInstance = False
self.server.newsInstance = False
self.server.defaultTimeline = 'tlblogs'
setConfigParam(baseDir,
"blogsInstance",
self.server.blogsInstance)
setConfigParam(baseDir,
"mediaInstance",
self.server.mediaInstance)
setConfigParam(baseDir,
"newsInstance",
self.server.newsInstance)
else:
if self.server.blogsInstance:
self.server.blogsInstance = False
self.server.defaultTimeline = 'inbox'
setConfigParam(baseDir,
"blogsInstance",
self.server.blogsInstance)
# change theme
if fields.get('themeDropdown'):
setTheme(baseDir,
fields['themeDropdown'],
domain)
setNewsAvatar(baseDir,
fields['themeDropdown'],
httpPrefix,
domain,
domainFull)
# change email address
currentEmailAddress = getEmailAddress(actorJson)
@ -3653,84 +3798,6 @@ class PubServer(BaseHTTPRequestHandler):
if currTheme:
setTheme(baseDir, currTheme, domain)
# change media instance status
if fields.get('mediaInstance'):
self.server.mediaInstance = False
self.server.defaultTimeline = 'inbox'
if fields['mediaInstance'] == 'on':
self.server.mediaInstance = True
self.server.blogsInstance = False
self.server.newsInstance = False
self.server.defaultTimeline = 'tlmedia'
setConfigParam(baseDir,
"mediaInstance",
self.server.mediaInstance)
setConfigParam(baseDir,
"blogsInstance",
self.server.blogsInstance)
setConfigParam(baseDir,
"newsInstance",
self.server.newsInstance)
else:
if self.server.mediaInstance:
self.server.mediaInstance = False
self.server.defaultTimeline = 'inbox'
setConfigParam(baseDir,
"mediaInstance",
self.server.mediaInstance)
# change news instance status
if fields.get('newsInstance'):
self.server.newsInstance = False
self.server.defaultTimeline = 'inbox'
if fields['newsInstance'] == 'on':
self.server.newsInstance = True
self.server.blogsInstance = False
self.server.mediaInstance = False
self.server.defaultTimeline = 'tlnews'
setConfigParam(baseDir,
"mediaInstance",
self.server.mediaInstance)
setConfigParam(baseDir,
"blogsInstance",
self.server.blogsInstance)
setConfigParam(baseDir,
"newsInstance",
self.server.newsInstance)
else:
if self.server.newsInstance:
self.server.newsInstance = False
self.server.defaultTimeline = 'inbox'
setConfigParam(baseDir,
"newsInstance",
self.server.mediaInstance)
# change blog instance status
if fields.get('blogsInstance'):
self.server.blogsInstance = False
self.server.defaultTimeline = 'inbox'
if fields['blogsInstance'] == 'on':
self.server.blogsInstance = True
self.server.mediaInstance = False
self.server.newsInstance = False
self.server.defaultTimeline = 'tlblogs'
setConfigParam(baseDir,
"blogsInstance",
self.server.blogsInstance)
setConfigParam(baseDir,
"mediaInstance",
self.server.mediaInstance)
setConfigParam(baseDir,
"newsInstance",
self.server.newsInstance)
else:
if self.server.blogsInstance:
self.server.blogsInstance = False
self.server.defaultTimeline = 'inbox'
setConfigParam(baseDir,
"blogsInstance",
self.server.blogsInstance)
# only receive DMs from accounts you follow
followDMsFilename = \
baseDir + '/accounts/' + \
@ -4211,7 +4278,8 @@ class PubServer(BaseHTTPRequestHandler):
nickname,
domain,
port,
maxPostsInRSSFeed, 1)
maxPostsInRSSFeed, 1,
True)
if msg is not None:
msg = msg.encode('utf-8')
self._set_headers('text/xml', len(msg),
@ -4229,6 +4297,66 @@ class PubServer(BaseHTTPRequestHandler):
path + ' ' + callingDomain)
self._404()
def _getRSS2site(self, authorized: bool,
callingDomain: str, path: str,
baseDir: str, httpPrefix: str,
domainFull: str, port: int, proxyType: str,
translate: {},
GETstartTime, GETtimings: {},
debug: bool):
"""Returns an RSS2 feed for all blogs on this instance
"""
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 = ''
for subdir, dirs, files in os.walk(baseDir + '/accounts'):
for acct in dirs:
if '@' not in acct:
continue
if 'inbox@' in acct or 'news@' in acct:
continue
nickname = acct.split('@')[0]
domain = acct.split('@')[1]
msg += \
htmlBlogPageRSS2(authorized,
self.server.session,
baseDir,
httpPrefix,
self.server.translate,
nickname,
domain,
port,
maxPostsInRSSFeed, 1,
False)
if msg:
msg = rss2Header(httpPrefix,
'news', domainFull,
'Site', translate) + msg + rss2Footer()
msg = msg.encode('utf-8')
self._set_headers('text/xml', len(msg),
None, callingDomain)
self._write(msg)
if debug:
print('Sent rss2 feed: ' +
path + ' ' + callingDomain)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'sharedInbox enabled',
'blog rss2')
return
if debug:
print('Failed to get rss2 feed: ' +
path + ' ' + callingDomain)
self._404()
def _getNewswireFeed(self, authorized: bool,
callingDomain: str, path: str,
baseDir: str, httpPrefix: str,
@ -4358,6 +4486,7 @@ class PubServer(BaseHTTPRequestHandler):
PGPfingerprint = getPGPfingerprint(actorJson)
msg = htmlPersonOptions(self.server.translate,
baseDir, domain,
domainFull,
originPathStr,
optionsActor,
optionsProfileUrl,
@ -5870,6 +5999,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.personCache,
YTReplacementDomain,
self.server.showPublishedDateOnly,
self.server.newswire,
actorJson['roles'],
None, None)
msg = msg.encode('utf-8')
@ -5943,6 +6073,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.personCache,
YTReplacementDomain,
showPublishedDateOnly,
self.server.newswire,
actorJson['skills'],
None, None)
msg = msg.encode('utf-8')
@ -7405,6 +7536,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.personCache,
self.server.YTReplacementDomain,
self.server.showPublishedDateOnly,
self.server.newswire,
shares,
pageNumber, sharesPerPage)
msg = msg.encode('utf-8')
@ -7492,6 +7624,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.personCache,
self.server.YTReplacementDomain,
self.server.showPublishedDateOnly,
self.server.newswire,
following,
pageNumber,
followsPerPage).encode('utf-8')
@ -7579,6 +7712,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.personCache,
self.server.YTReplacementDomain,
self.server.showPublishedDateOnly,
self.server.newswire,
followers,
pageNumber,
followsPerPage).encode('utf-8')
@ -7641,6 +7775,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.personCache,
self.server.YTReplacementDomain,
self.server.showPublishedDateOnly,
self.server.newswire,
None, None).encode('utf-8')
self._set_headers('text/html', len(msg),
cookie, callingDomain)
@ -7748,23 +7883,28 @@ class PubServer(BaseHTTPRequestHandler):
divertToLoginScreen = False
if divertToLoginScreen and not authorized:
if debug:
divertPath = '/login'
if self.server.newsInstance:
# for news instances if not logged in then show the
# front page
divertPath = '/users/news'
# if debug:
print('DEBUG: divertToLoginScreen=' +
str(divertToLoginScreen))
print('DEBUG: authorized=' + str(authorized))
print('DEBUG: path=' + path)
if callingDomain.endswith('.onion') and onionDomain:
self._redirect_headers('http://' +
onionDomain + '/login',
onionDomain + divertPath,
None, callingDomain)
elif callingDomain.endswith('.i2p') and i2pDomain:
self._redirect_headers('http://' +
i2pDomain + '/login',
i2pDomain + divertPath,
None, callingDomain)
else:
self._redirect_headers(httpPrefix + '://' +
domainFull +
'/login', None, callingDomain)
divertPath, None, callingDomain)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'robots txt',
'show login screen')
@ -8328,11 +8468,31 @@ class PubServer(BaseHTTPRequestHandler):
'_mastoApi(callingDomain)')
if self.path == '/logout':
if not self.server.newsInstance:
msg = \
htmlLogin(self.server.translate,
self.server.baseDir, False).encode('utf-8')
self._logout_headers('text/html', len(msg), callingDomain)
self._write(msg)
else:
if callingDomain.endswith('.onion') and \
self.server.onionDomain:
self._logout_redirect('http://' +
self.server.onionDomain +
'/users/news', None,
callingDomain)
elif (callingDomain.endswith('.i2p') and
self.server.i2pDomain):
self._logout_redirect('http://' +
self.server.i2pDomain +
'/users/news', None,
callingDomain)
else:
self._logout_redirect(self.server.httpPrefix +
'://' +
self.server.domainFull +
'/users/news',
None, callingDomain)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'_nodeinfo(callingDomain)',
'logout')
@ -8465,6 +8625,7 @@ class PubServer(BaseHTTPRequestHandler):
# RSS 2.0
if self.path.startswith('/blog/') and \
self.path.endswith('/rss.xml'):
if not self.path == '/blog/rss.xml':
self._getRSS2feed(authorized,
callingDomain, self.path,
self.server.baseDir,
@ -8474,6 +8635,17 @@ class PubServer(BaseHTTPRequestHandler):
self.server.proxyType,
GETstartTime, GETtimings,
self.server.debug)
else:
self._getRSS2site(authorized,
callingDomain, self.path,
self.server.baseDir,
self.server.httpPrefix,
self.server.domainFull,
self.server.port,
self.server.proxyType,
self.server.translate,
GETstartTime, GETtimings,
self.server.debug)
return
self._benchmarkGETtimings(GETstartTime, GETtimings,
@ -9067,8 +9239,11 @@ class PubServer(BaseHTTPRequestHandler):
'GET busy time',
'permitted directory')
if self.path.startswith('/login') or \
(self.path == '/' and not authorized):
# show the login screen
if (self.path.startswith('/login') or
(self.path == '/' and
not authorized and
not self.server.newsInstance)):
# request basic auth
msg = htmlLogin(self.server.translate,
self.server.baseDir).encode('utf-8')
@ -9080,6 +9255,33 @@ class PubServer(BaseHTTPRequestHandler):
'login shown')
return
# show the news front page
if self.path == '/' and \
not authorized and \
self.server.newsInstance:
if callingDomain.endswith('.onion') and \
self.server.onionDomain:
self._logout_redirect('http://' +
self.server.onionDomain +
'/users/news', None,
callingDomain)
elif (callingDomain.endswith('.i2p') and
self.server.i2pDomain):
self._logout_redirect('http://' +
self.server.i2pDomain +
'/users/news', None,
callingDomain)
else:
self._logout_redirect(self.server.httpPrefix +
'://' +
self.server.domainFull +
'/users/news',
None, callingDomain)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'permitted directory',
'news front page shown')
return
self._benchmarkGETtimings(GETstartTime, GETtimings,
'permitted directory',
'login shown done')
@ -11558,6 +11760,11 @@ class PubServer(BaseHTTPRequestHandler):
self.server.POSTbusy = False
return
if containsInvalidChars(messageBytes.decode("utf-8")):
self._400()
self.server.POSTbusy = False
return
# convert the raw bytes to json
messageJson = json.loads(messageBytes)
@ -11744,7 +11951,9 @@ def loadTokens(baseDir: str, tokensDict: {}, tokensLookup: {}) -> None:
tokensLookup[token] = nickname
def runDaemon(showPublishedDateOnly: bool,
def runDaemon(maxNewswireFeedSizeKb: int,
maxNewswirePostsPerSource: int,
showPublishedDateOnly: bool,
votingTimeMins: int,
positiveVoting: bool,
newswireVotesThreshold: int,
@ -11864,6 +12073,15 @@ def runDaemon(showPublishedDateOnly: bool,
# number of votes needed to remove a newswire item from the news timeline
# or if positive voting is anabled to add the item to the news timeline
httpd.newswireVotesThreshold = newswireVotesThreshold
# maximum overall size of an rss/atom feed read by the newswire daemon
# If the feed is too large then this is probably a DoS attempt
httpd.maxNewswireFeedSizeKb = maxNewswireFeedSizeKb
# For each newswire source (account or rss feed)
# this is the maximum number of posts to show for each.
# This avoids one or two sources from dominating the news,
# and also prevents big feeds from slowing down page load times
httpd.maxNewswirePostsPerSource = maxNewswirePostsPerSource
# Show only the date at the bottom of posts, and not the time
httpd.showPublishedDateOnly = showPublishedDateOnly
@ -11929,6 +12147,16 @@ def runDaemon(showPublishedDateOnly: bool,
print('Creating news inbox: news@' + domain)
createNewsInbox(baseDir, domain, port, httpPrefix)
# set the avatar for the news account
themeName = getConfigParam(baseDir, 'theme')
if not themeName:
themeName = 'default'
setNewsAvatar(baseDir,
themeName,
httpPrefix,
domain,
httpd.domainFull)
if not os.path.isdir(baseDir + '/cache'):
os.mkdir(baseDir + '/cache')
if not os.path.isdir(baseDir + '/cache/actors'):

View File

@ -41,7 +41,9 @@
--font-size-tox2: 8px;
--time-color: #aaa;
--time-vertical-align: 4px;
--publish-button-text: #FFFFFF;
--button-text: #FFFFFF;
--publish-button-background: #999;
--button-background: #999;
--button-background-hover: #777;
--button-selected: #666;
@ -50,6 +52,7 @@
--button-selected-highlighted: darkgreen;
--button-approve: darkgreen;
--button-deny: darkred;
--button-width-chars: 10ch;
--button-height: 10px;
--button-height-padding-mobile: 20px;
--button-height-padding: 10px;
@ -68,7 +71,7 @@
--quote-font-weight: normal;
--quote-font-size: 120%;
--line-spacing: 130%;
--line-spacing-newswire: 100%;
--line-spacing-newswire: 120%;
--newswire-item-moderated-color: white;
--newswire-date-moderated-color: white;
--column-left-width: 10vw;
@ -81,9 +84,13 @@
--column-left-icon-size: 20%;
--column-left-icon-size-mobile: 10%;
--column-left-image-width-mobile: 40vw;
--column-right-image-width-mobile: 100vw;
--column-right-icon-size: 20%;
--column-right-icon-size-mobile: 10%;
--newswire-date-color: white;
--newswire-voted-background-color: black;
--login-button-color: #2965;
--login-button-fg-color: black;
}
@font-face {
@ -577,8 +584,8 @@ input[type=submit] {
}
.loginButton {
background-color: #2965;
color: #000;
background-color: var(--login-button-color);
color: var(--login-button-fg-color);
float: none;
margin: 0px 10px;
padding: 12px 40px;
@ -1224,11 +1231,27 @@ aside .toggle-inside li {
padding: var(--button-height-padding);
width: 10%;
max-width: 200px;
min-width: 10ch;
min-width: var(--button-width-chars);
transition: all 0.5s;
cursor: pointer;
margin: 5px;
}
.publishbtn {
border-radius: var(--button-corner-radius);
background-color: var(--publish-button-background);
border: none;
color: var(--publish-button-text);
text-align: center;
font-size: var(--font-size-header);
font-family: Arial, Helvetica, sans-serif;
padding: var(--button-height-padding);
width: 10%;
max-width: 200px;
min-width: var(--button-width-chars);
transition: all 0.5s;
cursor: pointer;
margin: -20px 0px;
}
.buttonhighlighted {
border-radius: var(--button-corner-radius);
background-color: var(--button-highlighted);
@ -1240,7 +1263,7 @@ aside .toggle-inside li {
padding: var(--button-height-padding);
width: 10%;
max-width: 100px;
min-width: 10ch;
min-width: var(--button-width-chars);
transition: all 0.5s;
cursor: pointer;
margin: 5px;
@ -1256,7 +1279,7 @@ aside .toggle-inside li {
padding: var(--button-height-padding);
width: 10%;
max-width: 100px;
min-width: 10ch;
min-width: var(--button-width-chars);
transition: all 0.5s;
cursor: pointer;
margin: 5px;
@ -1272,7 +1295,7 @@ aside .toggle-inside li {
padding: var(--button-height-padding);
width: 10%;
max-width: 100px;
min-width: 10ch;
min-width: var(--button-width-chars);
transition: all 0.5s;
cursor: pointer;
margin: 5px;
@ -1566,13 +1589,14 @@ aside .toggle-inside li {
}
.rightColEditImage {
background: var(--main-bg-color);
width: var(--column-right-icon-size);
width: var(--column-right-icon-size-mobile);
float: right;
margin: 20px 0px;
}
.rightColImg {
background: var(--main-bg-color);
width: 100vw;
width: var(--column-right-image-width-mobile);
float: right;
margin: 0 0;
padding: 0 0;
}
@ -1790,7 +1814,23 @@ aside .toggle-inside li {
padding: var(--button-height-padding-mobile);
width: 20%;
max-width: 400px;
min-width: 10ch;
min-width: var(--button-width-chars);
transition: all 0.5s;
cursor: pointer;
margin: 15px;
}
.publishbtn {
border-radius: var(--button-corner-radius);
background-color: var(--publish-button-background);
border: none;
color: var(--publish-button-text);
text-align: center;
font-size: var(--font-size-newswire-mobile);
font-family: Arial, Helvetica, sans-serif;
padding: var(--button-height-padding-mobile);
width: 20%;
max-width: 400px;
min-width: var(--button-width-chars);
transition: all 0.5s;
cursor: pointer;
margin: 15px;
@ -1806,7 +1846,7 @@ aside .toggle-inside li {
padding: var(--button-height-padding-mobile);
width: 20%;
max-width: 400px;
min-width: 10ch;
min-width: var(--button-width-chars);
transition: all 0.5s;
cursor: pointer;
margin: 15px;
@ -1822,7 +1862,7 @@ aside .toggle-inside li {
padding: var(--button-height-padding-mobile);
width: 20%;
max-width: 400px;
min-width: 10ch;
min-width: var(--button-width-chars);
transition: all 0.5s;
cursor: pointer;
margin: 15px;
@ -1838,7 +1878,7 @@ aside .toggle-inside li {
padding: var(--button-height-padding-mobile);
width: 20%;
max-width: 400px;
min-width: 10ch;
min-width: var(--button-width-chars);
transition: all 0.5s;
cursor: pointer;
margin: 15px;

View File

@ -112,6 +112,14 @@ parser.add_argument('--i2pDomain', dest='i2pDomain', type=str,
parser.add_argument('-p', '--port', dest='port', type=int,
default=None,
help='Port number to run on')
parser.add_argument('--postsPerSource',
dest='maxNewswirePostsPerSource', type=int,
default=5,
help='Maximum newswire posts per feed or account')
parser.add_argument('--maxFeedSize',
dest='maxNewswireFeedSizeKb', type=int,
default=2048,
help='Maximum newswire rss/atom feed size in K')
parser.add_argument('--postcache', dest='maxRecentPosts', type=int,
default=512,
help='The maximum number of recent posts to store in RAM')
@ -1925,6 +1933,20 @@ dateonly = getConfigParam(baseDir, 'dateonly')
if dateonly:
args.dateonly = dateonly
# set the maximum number of newswire posts per account or rss feed
maxNewswirePostsPerSource = \
getConfigParam(baseDir, 'maxNewswirePostsPerSource')
if maxNewswirePostsPerSource:
if maxNewswirePostsPerSource.isdigit():
args.maxNewswirePostsPerSource = maxNewswirePostsPerSource
# set the maximum size of a newswire rss/atom feed in Kilobytes
maxNewswireFeedSizeKb = \
getConfigParam(baseDir, 'maxNewswireFeedSizeKb')
if maxNewswireFeedSizeKb:
if maxNewswireFeedSizeKb.isdigit():
args.maxNewswireFeedSizeKb = maxNewswireFeedSizeKb
YTDomain = getConfigParam(baseDir, 'youtubedomain')
if YTDomain:
if '://' in YTDomain:
@ -1938,7 +1960,9 @@ if setTheme(baseDir, themeName, domain):
print('Theme set to ' + themeName)
if __name__ == "__main__":
runDaemon(args.dateonly,
runDaemon(args.maxNewswireFeedSizeKb,
args.maxNewswirePostsPerSource,
args.dateonly,
args.votingtime,
args.positivevoting,
args.minimumvotes,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 95 KiB

View File

@ -202,9 +202,8 @@ def mergeWithPreviousNewswire(oldNewswire: {}, newNewswire: {}) -> None:
for published, fields in oldNewswire.items():
if not newNewswire.get(published):
continue
newNewswire[published][1] = fields[1]
newNewswire[published][2] = fields[2]
newNewswire[published][3] = fields[3]
for i in range(1, 5):
newNewswire[published][i] = fields[i]
def runNewswireDaemon(baseDir: str, httpd,
@ -226,7 +225,10 @@ def runNewswireDaemon(baseDir: str, httpd,
# try to update the feeds
newNewswire = None
try:
newNewswire = getDictFromNewswire(httpd.session, baseDir)
newNewswire = \
getDictFromNewswire(httpd.session, baseDir,
httpd.maxNewswirePostsPerSource,
httpd.maxNewswireFeedSizeKb)
except Exception as e:
print('WARN: unable to update newswire ' + str(e))
time.sleep(120)

View File

@ -16,6 +16,8 @@ from utils import locatePost
from utils import loadJson
from utils import saveJson
from utils import isSuspended
from utils import containsInvalidChars
from blocking import isBlockedDomain
def rss2Header(httpPrefix: str,
@ -26,14 +28,17 @@ def rss2Header(httpPrefix: 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>'
elif title.startswith('Site'):
rssStr += ' <title>' + domainFull + '</title>'
rssStr += ' <link>' + httpPrefix + '://' + domainFull + \
'/blog/rss.xml' + '</link>'
else:
rssStr += ' <title>' + translate[title] + '</title>'
rssStr += ' <link>' + httpPrefix + '://' + domainFull + \
'/users/' + nickname + '/rss.xml' + '</link>'
return rssStr
@ -47,13 +52,15 @@ def rss2Footer() -> str:
return rssStr
def xml2StrToDict(xmlStr: str, moderated: bool) -> {}:
def xml2StrToDict(baseDir: str, xmlStr: str, moderated: bool,
maxPostsPerSource: int) -> {}:
"""Converts an xml 2.0 string to a dictionary
"""
if '<item>' not in xmlStr:
return {}
result = {}
rssItems = xmlStr.split('<item>')
postCtr = 0
for rssItem in rssItems:
if '<title>' not in rssItem:
continue
@ -75,6 +82,13 @@ def xml2StrToDict(xmlStr: str, moderated: bool) -> {}:
description = description.split('</description>')[0]
link = rssItem.split('<link>')[1]
link = link.split('</link>')[0]
if '://' not in link:
continue
domain = link.split('://')[1]
if '/' in domain:
domain = domain.split('/')[0]
if isBlockedDomain(baseDir, domain):
continue
pubDate = rssItem.split('<pubDate>')[1]
pubDate = pubDate.split('</pubDate>')[0]
parsed = False
@ -86,6 +100,9 @@ def xml2StrToDict(xmlStr: str, moderated: bool) -> {}:
result[str(publishedDate)] = [title, link,
votesStatus, postFilename,
description, moderated]
postCtr += 1
if postCtr >= maxPostsPerSource:
break
parsed = True
except BaseException:
pass
@ -93,7 +110,15 @@ def xml2StrToDict(xmlStr: str, moderated: bool) -> {}:
try:
publishedDate = \
datetime.strptime(pubDate, "%a, %d %b %Y %H:%M:%S UT")
result[str(publishedDate) + '+00:00'] = [title, link]
postFilename = ''
votesStatus = []
result[str(publishedDate) + '+00:00'] = \
[title, link,
votesStatus, postFilename,
description, moderated]
postCtr += 1
if postCtr >= maxPostsPerSource:
break
parsed = True
except BaseException:
print('WARN: unrecognized RSS date format: ' + pubDate)
@ -101,13 +126,15 @@ def xml2StrToDict(xmlStr: str, moderated: bool) -> {}:
return result
def atomFeedToDict(xmlStr: str, moderated: bool) -> {}:
def atomFeedToDict(baseDir: str, xmlStr: str, moderated: bool,
maxPostsPerSource: int) -> {}:
"""Converts an atom feed string to a dictionary
"""
if '<entry>' not in xmlStr:
return {}
result = {}
rssItems = xmlStr.split('<entry>')
postCtr = 0
for rssItem in rssItems:
if '<title>' not in rssItem:
continue
@ -129,6 +156,13 @@ def atomFeedToDict(xmlStr: str, moderated: bool) -> {}:
description = description.split('</summary>')[0]
link = rssItem.split('<link>')[1]
link = link.split('</link>')[0]
if '://' not in link:
continue
domain = link.split('://')[1]
if '/' in domain:
domain = domain.split('/')[0]
if isBlockedDomain(baseDir, domain):
continue
pubDate = rssItem.split('<updated>')[1]
pubDate = pubDate.split('</updated>')[0]
parsed = False
@ -140,6 +174,9 @@ def atomFeedToDict(xmlStr: str, moderated: bool) -> {}:
result[str(publishedDate)] = [title, link,
votesStatus, postFilename,
description, moderated]
postCtr += 1
if postCtr >= maxPostsPerSource:
break
parsed = True
except BaseException:
pass
@ -147,7 +184,15 @@ def atomFeedToDict(xmlStr: str, moderated: bool) -> {}:
try:
publishedDate = \
datetime.strptime(pubDate, "%a, %d %b %Y %H:%M:%S UT")
result[str(publishedDate) + '+00:00'] = [title, link]
postFilename = ''
votesStatus = []
result[str(publishedDate) + '+00:00'] = \
[title, link,
votesStatus, postFilename,
description, moderated]
postCtr += 1
if postCtr >= maxPostsPerSource:
break
parsed = True
except BaseException:
print('WARN: unrecognized atom feed date format: ' + pubDate)
@ -155,17 +200,20 @@ def atomFeedToDict(xmlStr: str, moderated: bool) -> {}:
return result
def xmlStrToDict(xmlStr: str, moderated: bool) -> {}:
def xmlStrToDict(baseDir: str, xmlStr: str, moderated: bool,
maxPostsPerSource: int) -> {}:
"""Converts an xml string to a dictionary
"""
if 'rss version="2.0"' in xmlStr:
return xml2StrToDict(xmlStr, moderated)
return xml2StrToDict(baseDir, xmlStr, moderated, maxPostsPerSource)
elif 'xmlns="http://www.w3.org/2005/Atom"' in xmlStr:
return atomFeedToDict(xmlStr, moderated)
return atomFeedToDict(baseDir, xmlStr, moderated, maxPostsPerSource)
return {}
def getRSS(session, url: str, moderated: bool) -> {}:
def getRSS(baseDir: str, session, url: str, moderated: bool,
maxPostsPerSource: int,
maxFeedSizeKb: int) -> {}:
"""Returns an RSS url as a dict
"""
if not isinstance(url, str):
@ -188,7 +236,13 @@ def getRSS(session, url: str, moderated: bool) -> {}:
print('WARN: no session specified for getRSS')
try:
result = session.get(url, headers=sessionHeaders, params=sessionParams)
return xmlStrToDict(result.text, moderated)
if result:
if int(len(result.text) / 1024) < maxFeedSizeKb and \
not containsInvalidChars(result.text):
return xmlStrToDict(baseDir, result.text, moderated,
maxPostsPerSource)
else:
print('WARN: feed is too large: ' + url)
except requests.exceptions.RequestException as e:
print('ERROR: getRSS failed\nurl: ' + str(url) + '\n' +
'headers: ' + str(sessionHeaders) + '\n' +
@ -365,13 +419,16 @@ def addBlogsToNewswire(baseDir: str, newswire: {},
os.remove(newswireModerationFilename)
def getDictFromNewswire(session, baseDir: str) -> {}:
def getDictFromNewswire(session, baseDir: str,
maxPostsPerSource: int, maxFeedSizeKb: int) -> {}:
"""Gets rss feeds as a dictionary from newswire file
"""
subscriptionsFilename = baseDir + '/accounts/newswire.txt'
if not os.path.isfile(subscriptionsFilename):
return {}
maxPostsPerSource = 5
# add rss feeds
rssFeed = []
with open(subscriptionsFilename, 'r') as fp:
@ -394,12 +451,13 @@ def getDictFromNewswire(session, baseDir: str) -> {}:
moderated = True
url = url.replace('*', '').strip()
itemsList = getRSS(session, url, moderated)
itemsList = getRSS(baseDir, session, url, moderated,
maxPostsPerSource, maxFeedSizeKb)
for dateStr, item in itemsList.items():
result[dateStr] = item
# add blogs from each user account
addBlogsToNewswire(baseDir, result, 5)
addBlogsToNewswire(baseDir, result, maxPostsPerSource)
# sort into chronological order, latest first
sortedResult = OrderedDict(sorted(result.items(), reverse=True))

View File

@ -1211,13 +1211,12 @@ def createPublicPost(baseDir: str,
def createBlogPost(baseDir: str,
nickname: str, domain: str, port: int, httpPrefix: str,
content: str, followersOnly: bool, saveToFile: bool,
clientToServer: bool,
clientToServer: bool, commentsEnabled: bool,
attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool,
inReplyTo=None, inReplyToAtomUri=None, subject=None,
schedulePost=False,
eventDate=None, eventTime=None, location=None) -> {}:
commentsEnabled = True
blog = \
createPublicPost(baseDir,
nickname, domain, port, httpPrefix,
@ -3532,6 +3531,7 @@ def rejectAnnounce(announceFilename: str):
"""
if not os.path.isfile(announceFilename + '.reject'):
rejectAnnounceFile = open(announceFilename + '.reject', "w+")
if rejectAnnounceFile:
rejectAnnounceFile.write('\n')
rejectAnnounceFile.close()

View File

@ -288,7 +288,7 @@ def createServerAlice(path: str, domain: str, port: int,
onionDomain = None
i2pDomain = None
print('Server running: Alice')
runDaemon(False, 0, False, 1, False, False, False,
runDaemon(1024, 5, False, 0, False, 1, False, False, False,
5, True, True, 'en', __version__,
"instanceId", False, path, domain,
onionDomain, i2pDomain, None, port, port,
@ -351,7 +351,7 @@ def createServerBob(path: str, domain: str, port: int,
onionDomain = None
i2pDomain = None
print('Server running: Bob')
runDaemon(False, 0, False, 1, False, False, False,
runDaemon(1024, 5, False, 0, False, 1, False, False, False,
5, True, True, 'en', __version__,
"instanceId", False, path, domain,
onionDomain, i2pDomain, None, port, port,
@ -388,7 +388,7 @@ def createServerEve(path: str, domain: str, port: int, federationList: [],
onionDomain = None
i2pDomain = None
print('Server running: Eve')
runDaemon(False, 0, False, 1, False, False, False,
runDaemon(1024, 5, False, 0, False, 1, False, False, False,
5, True, True, 'en', __version__,
"instanceId", False, path, domain,
onionDomain, i2pDomain, None, port, port,

View File

@ -293,6 +293,8 @@ def setThemeIndymedia(baseDir: str):
"hashtag-vertical-spacing3": "100px",
"hashtag-vertical-spacing4": "150px",
"button-background-hover": "darkblue",
"publish-button-background": "#ff9900",
"publish-button-text": "#003366",
"button-background": "#003366",
"button-selected": "blue",
"calendar-bg-color": "#0f0d10",
@ -310,7 +312,9 @@ def setThemeIndymedia(baseDir: str):
"column-left-width": "10vw",
"column-center-width": "70vw",
"column-right-width": "20vw",
"column-right-icon-size": "11%"
"column-right-icon-size": "11%",
"login-button-color": "red",
"login-button-fg-color": "white"
}
setThemeFromDict(baseDir, name, themeParams, bgParams)
@ -320,6 +324,7 @@ def setThemeBlue(baseDir: str):
removeTheme(baseDir)
setThemeInConfig(baseDir, name)
themeParams = {
"newswire-date-color": "blue",
"font-size-header": "22px",
"font-size-header-mobile": "32px",
"font-size": "45px",
@ -373,17 +378,18 @@ def setThemeNight(baseDir: str):
"link-bg-color": "#0f0d10",
"main-link-color": "ff9900",
"main-link-color-hover": "#d09338",
"main-fg-color": "#a961ab",
"column-left-fg-color": "#a961ab",
"main-fg-color": "#0481f5",
"column-left-fg-color": "#0481f5",
"main-bg-color-dm": "#0b0a0a",
"border-color": "#606984",
"main-bg-color-reply": "#0f0d10",
"main-bg-color-report": "#0f0d10",
"hashtag-vertical-spacing3": "100px",
"hashtag-vertical-spacing4": "150px",
"button-background-hover": "#6961ab",
"button-background": "#a961ab",
"button-selected": "#86579d",
"button-background-hover": "#0481f5",
"publish-button-background": "#07447c",
"button-background": "#07447c",
"button-selected": "#0481f5",
"calendar-bg-color": "#0f0d10",
"lines-color": "#a961ab",
"day-number": "#a961ab",
@ -412,6 +418,8 @@ def setThemeStarlight(baseDir: str):
removeTheme(baseDir)
setThemeInConfig(baseDir, name)
themeParams = {
"column-left-image-width-mobile": "40vw",
"line-spacing-newswire": "120%",
"focus-color": "darkred",
"font-size-button-mobile": "36px",
"font-size": "32px",
@ -437,6 +445,7 @@ def setThemeStarlight(baseDir: str):
"hashtag-vertical-spacing3": "100px",
"hashtag-vertical-spacing4": "150px",
"button-background-hover": "#a9282c",
"publish-button-background": "#69282c",
"button-background": "#69282c",
"button-small-background": "darkblue",
"button-selected": "#a34046",
@ -474,6 +483,8 @@ def setThemeHenge(baseDir: str):
removeTheme(baseDir)
setThemeInConfig(baseDir, name)
themeParams = {
"column-left-image-width-mobile": "40vw",
"column-right-image-width-mobile": "40vw",
"font-size-button-mobile": "36px",
"font-size": "32px",
"font-size2": "26px",
@ -498,6 +509,7 @@ def setThemeHenge(baseDir: str):
"hashtag-vertical-spacing3": "100px",
"hashtag-vertical-spacing4": "150px",
"button-background-hover": "#444",
"publish-button-background": "#222",
"button-background": "#222",
"button-selected": "black",
"dropdown-fg-color": "#dddddd",
@ -545,6 +557,7 @@ def setThemeZen(baseDir: str):
"title-color": "#dddddd",
"main-visited-color": "#dddddd",
"button-background-hover": "#a63b35",
"publish-button-background": "#463b35",
"button-background": "#463b35",
"button-selected": "#26201d",
"main-bg-color-dm": "#5c4a40",
@ -591,8 +604,12 @@ def setThemeHighVis(baseDir: str):
def setThemeLCD(baseDir: str):
name = 'lcd'
themeParams = {
"newswire-date-color": "#cfb42b",
"column-left-header-background": "#9fb42b",
"column-left-header-color": "#33390d",
"main-bg-color": "#9fb42b",
"column-left-color": "#9fb42b",
"column-left-color": "#33390d",
"column-left-fg-color": "#9fb42b",
"link-bg-color": "#33390d",
"text-entry-foreground": "#33390d",
"text-entry-background": "#9fb42b",
@ -601,7 +618,6 @@ 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",
@ -611,9 +627,11 @@ def setThemeLCD(baseDir: str):
"button-selected": "black",
"button-highlighted": "green",
"button-background-hover": "#a3390d",
"publish-button-background": "#33390d",
"button-background": "#33390d",
"button-small-background": "#33390d",
"button-text": "#9fb42b",
"publish-button-text": "#9fb42b",
"button-small-text": "#9fb42b",
"color: #FFFFFE;": "color: #9fb42b;",
"calendar-bg-color": "#eee",
@ -684,9 +702,11 @@ def setThemePurple(baseDir: str):
"main-visited-color": "#f93bb0",
"button-selected": "#c042a0",
"button-background-hover": "#af42a0",
"publish-button-background": "#ff42a0",
"button-background": "#ff42a0",
"button-small-background": "#ff42a0",
"button-text": "white",
"publish-button-text": "white",
"button-small-text": "white",
"color: #FFFFFE;": "color: #1f152d;",
"calendar-bg-color": "#eee",
@ -735,9 +755,11 @@ def setThemeHacker(baseDir: str):
"main-visited-color": "#3c8234",
"button-selected": "#063200",
"button-background-hover": "#a62200",
"publish-button-background": "#062200",
"button-background": "#062200",
"button-small-background": "#062200",
"button-text": "#00ff00",
"publish-button-text": "#00ff00",
"button-small-text": "#00ff00",
"button-corner-radius": "4px",
"timeline-border-radius": "4px",
@ -830,6 +852,9 @@ def setThemeLight(baseDir: str):
def setThemeSolidaric(baseDir: str):
name = 'solidaric'
themeParams = {
"button-highlighted": "darkred",
"button-selected-highlighted": "darkred",
"newswire-date-color": "grey",
"focus-color": "grey",
"font-size-button-mobile": "36px",
"font-size": "32px",
@ -1006,6 +1031,31 @@ def setThemeImages(baseDir: str, name: str) -> None:
pass
def setNewsAvatar(baseDir: str, name: str,
httpPrefix: str,
domain: str, domainFull: str) -> None:
"""Sets the avatar for the news account
"""
nickname = 'news'
newFilename = baseDir + '/img/icons/' + name + '/avatar_news.png'
if not os.path.isfile(newFilename):
newFilename = baseDir + '/img/icons/avatar_news.png'
if not os.path.isfile(newFilename):
return
avatarFilename = \
httpPrefix + '://' + domainFull + '/users/' + nickname + '.png'
avatarFilename = avatarFilename.replace('/', '-')
filename = baseDir + '/cache/avatars/' + avatarFilename
if os.path.isfile(filename):
os.remove(filename)
if os.path.isdir(baseDir + '/cache/avatars'):
copyfile(newFilename, filename)
copyfile(newFilename,
baseDir + '/accounts/' +
nickname + '@' + domain + '/avatar.png')
def setTheme(baseDir: str, name: str, domain: str) -> bool:
result = False

View File

@ -308,5 +308,8 @@
"Read more...": "اقرأ أكثر...",
"Edit News Post": "تحرير منشور الأخبار",
"A list of editor nicknames. One per line.": "قائمة بأسماء المحرر. واحد في كل سطر.",
"Site Editors": "محررو الموقع"
"Site Editors": "محررو الموقع",
"Allow news posts": "السماح بنشر الأخبار",
"Publish": "ينشر",
"Publish a news article": "انشر مقالة إخبارية"
}

View File

@ -308,5 +308,8 @@
"Read more...": "Llegeix més...",
"Edit News Post": "Edita la publicació de notícies",
"A list of editor nicknames. One per line.": "Una llista de sobrenoms de l'editor. Un per línia.",
"Site Editors": "Editors de llocs"
"Site Editors": "Editors de llocs",
"Allow news posts": "Permet publicacions de notícies",
"Publish": "Publica",
"Publish a news article": "Publicar un article de notícies"
}

View File

@ -308,5 +308,8 @@
"Read more...": "Darllen mwy...",
"Edit News Post": "Golygu News News",
"A list of editor nicknames. One per line.": "Rhestr o lysenwau golygydd. Un i bob llinell.",
"Site Editors": "Golygyddion Safle"
"Site Editors": "Golygyddion Safle",
"Allow news posts": "Caniatáu swyddi newyddion",
"Publish": "Cyhoeddi",
"Publish a news article": "Cyhoeddi erthygl newyddion"
}

View File

@ -308,5 +308,8 @@
"Read more...": "Weiterlesen...",
"Edit News Post": "Nachrichtenbeitrag bearbeiten",
"A list of editor nicknames. One per line.": "Eine Liste der Editor-Spitznamen. Eine pro Zeile.",
"Site Editors": "Site-Editoren"
"Site Editors": "Site-Editoren",
"Allow news posts": "Nachrichtenbeiträge zulassen",
"Publish": "Veröffentlichen",
"Publish a news article": "Veröffentlichen Sie einen Nachrichtenartikel"
}

View File

@ -308,5 +308,8 @@
"Read more...": "Read more...",
"Edit News Post": "Edit News Post",
"A list of editor nicknames. One per line.": "A list of editor nicknames. One per line.",
"Site Editors": "Site Editors"
"Site Editors": "Site Editors",
"Allow news posts": "Allow news posts",
"Publish": "Publish",
"Publish a news article": "Publish a news article"
}

View File

@ -308,5 +308,8 @@
"Read more...": "Lee mas...",
"Edit News Post": "Editar publicación de noticias",
"A list of editor nicknames. One per line.": "Una lista de apodos de los editores. Uno por línea.",
"Site Editors": "Editores del sitio"
"Site Editors": "Editores del sitio",
"Allow news posts": "Permitir publicaciones de noticias",
"Publish": "Publicar",
"Publish a news article": "Publica un artículo de noticias"
}

View File

@ -308,5 +308,8 @@
"Read more...": "Lire la suite...",
"Edit News Post": "Modifier l'article d'actualité",
"A list of editor nicknames. One per line.": "Une liste de surnoms d'éditeur. Un par ligne.",
"Site Editors": "Éditeurs du site"
"Site Editors": "Éditeurs du site",
"Allow news posts": "Autoriser les articles d'actualité",
"Publish": "Publier",
"Publish a news article": "Publier un article de presse"
}

View File

@ -308,5 +308,8 @@
"Read more...": "Leigh Nios mo...",
"Edit News Post": "Cuir News Post in eagar",
"A list of editor nicknames. One per line.": "Liosta leasainmneacha eagarthóra. Ceann in aghaidh na líne.",
"Site Editors": "Eagarthóirí Suímh"
"Site Editors": "Eagarthóirí Suímh",
"Allow news posts": "Ceadaigh poist nuachta",
"Publish": "Fhoilsiú",
"Publish a news article": "Foilsigh alt nuachta"
}

View File

@ -308,5 +308,8 @@
"Read more...": "अधिक पढ़ें...",
"Edit News Post": "समाचार पोस्ट संपादित करें",
"A list of editor nicknames. One per line.": "संपादक उपनामों की एक सूची। प्रति पंक्ति एक।",
"Site Editors": "साइट संपादकों"
"Site Editors": "साइट संपादकों",
"Allow news posts": "समाचार पोस्ट की अनुमति दें",
"Publish": "प्रकाशित करना",
"Publish a news article": "एक समाचार लेख प्रकाशित करें"
}

View File

@ -308,5 +308,8 @@
"Read more...": "Leggi di più...",
"Edit News Post": "Modifica post di notizie",
"A list of editor nicknames. One per line.": "Un elenco di soprannomi dell'editor. Uno per riga.",
"Site Editors": "Editori del sito"
"Site Editors": "Editori del sito",
"Allow news posts": "Consenti post di notizie",
"Publish": "Pubblicare",
"Publish a news article": "Pubblica un articolo di notizie"
}

View File

@ -308,5 +308,8 @@
"Read more...": "続きを読む...",
"Edit News Post": "ニュース投稿を編集する",
"A list of editor nicknames. One per line.": "編集者のニックネームのリスト。 1行に1つ。",
"Site Editors": "サイト編集者"
"Site Editors": "サイト編集者",
"Allow news posts": "ニュース投稿を許可する",
"Publish": "公開する",
"Publish a news article": "ニュース記事を公開する"
}

View File

@ -304,5 +304,8 @@
"Read more...": "Read more...",
"Edit News Post": "Edit News Post",
"A list of editor nicknames. One per line.": "A list of editor nicknames. One per line.",
"Site Editors": "Site Editors"
"Site Editors": "Site Editors",
"Allow news posts": "Allow news posts",
"Publish": "Publish",
"Publish a news article": "Publish a news article"
}

View File

@ -308,5 +308,8 @@
"Read more...": "Consulte Mais informação...",
"Edit News Post": "Editar Postagem de Notícias",
"A list of editor nicknames. One per line.": "Uma lista de apelidos de editores. Um por linha.",
"Site Editors": "Editores do site"
"Site Editors": "Editores do site",
"Allow news posts": "Permitir postagens de notícias",
"Publish": "Publicar",
"Publish a news article": "Publique um artigo de notícias"
}

View File

@ -308,5 +308,8 @@
"Read more...": "Подробнее...",
"Edit News Post": "Редактировать новость",
"A list of editor nicknames. One per line.": "Список ников редакторов. По одному на строку.",
"Site Editors": "Редакторы сайта"
"Site Editors": "Редакторы сайта",
"Allow news posts": "Разрешить публикации новостей",
"Publish": "Публиковать",
"Publish a news article": "Опубликовать новостную статью"
}

View File

@ -308,5 +308,8 @@
"Read more...": "阅读更多...",
"Edit News Post": "编辑新闻帖子",
"A list of editor nicknames. One per line.": "编辑者昵称列表。 每行一个。",
"Site Editors": "网站编辑"
"Site Editors": "网站编辑",
"Allow news posts": "允许新闻发布",
"Publish": "发布",
"Publish a news article": "发布新闻文章"
}

View File

@ -19,6 +19,14 @@ from calendar import monthrange
from followingCalendar import addPersonToCalendar
def isSystemAccount(nickname: str) -> bool:
"""Returns true if the given nickname is a system account
"""
if nickname == 'news' or nickname == 'inbox':
return True
return False
def createConfig(baseDir: str) -> None:
"""Creates a configuration file
"""
@ -49,7 +57,7 @@ def getConfigParam(baseDir: str, variableName: str):
configFilename = baseDir + '/config.json'
configJson = loadJson(configFilename)
if configJson:
if configJson.get(variableName):
if variableName in configJson:
return configJson[variableName]
return None
@ -265,6 +273,19 @@ def isEvil(domain: str) -> bool:
return False
def containsInvalidChars(jsonStr: str) -> bool:
"""Does the given json string contain invalid characters?
e.g. dubious clacks/admin dogwhistles
"""
invalidStrings = {
'', '', '', '', '', ''
}
for isInvalid in invalidStrings:
if isInvalid in jsonStr:
return True
return False
def createPersonDir(nickname: str, domain: str, baseDir: str,
dirname: str) -> str:
"""Create a directory for a person

View File

@ -25,6 +25,7 @@ from ssb import getSSBAddress
from tox import getToxAddress
from matrix import getMatrixAddress
from donate import getDonationUrl
from utils import isSystemAccount
from utils import removeIdEnding
from utils import getProtocolPrefixes
from utils import searchBoxPosts
@ -3232,7 +3233,7 @@ def htmlProfile(defaultTimeline: str,
session, wfRequest: {}, personCache: {},
YTReplacementDomain: str,
showPublishedDateOnly: bool,
extraJson=None,
newswire: {}, extraJson=None,
pageNumber=None, maxItemsPerPage=None) -> str:
"""Show the profile page as html
"""
@ -3296,7 +3297,7 @@ def htmlProfile(defaultTimeline: str,
PGPfingerprint or emailAddress:
donateSection = '<div class="container">\n'
donateSection += ' <center>\n'
if donateUrl:
if donateUrl and not isSystemAccount(nickname):
donateSection += \
' <p><a href="' + donateUrl + \
'"><button class="donateButton">' + translate['Donate'] + \
@ -3415,13 +3416,37 @@ def htmlProfile(defaultTimeline: str,
avatarDescription = profileJson['summary'].replace('<br>', '\n')
avatarDescription = avatarDescription.replace('<p>', '')
avatarDescription = avatarDescription.replace('</p>', '')
profileHeaderStr = '<div class="hero-image">'
profileHeaderStr += ' <div class="hero-text">'
# If this is the news account then show a different banner
if isSystemAccount(nickname):
profileHeaderStr = '<div class="timeline-banner"></div>\n'
profileHeaderStr += '<center>' + loginButton + '</center>\n'
profileHeaderStr += '<table class="timeline">\n'
profileHeaderStr += ' <colgroup>\n'
profileHeaderStr += ' <col span="1" class="column-left">\n'
profileHeaderStr += ' <col span="1" class="column-center">\n'
profileHeaderStr += ' <col span="1" class="column-right">\n'
profileHeaderStr += ' </colgroup>\n'
profileHeaderStr += ' <tbody>\n'
profileHeaderStr += ' <tr>\n'
profileHeaderStr += ' <td valign="top" class="col-left">\n'
iconsDir = getIconsDir(baseDir)
profileHeaderStr += \
getLeftColumnContent(baseDir, 'news', domainFull,
httpPrefix, translate,
iconsDir, False,
False, None)
profileHeaderStr += ' </td>\n'
profileHeaderStr += ' <td valign="top" class="col-center">\n'
else:
profileHeaderStr = '<div class="hero-image">\n'
profileHeaderStr += ' <div class="hero-text">\n'
profileHeaderStr += \
' <img loading="lazy" src="' + profileJson['icon']['url'] + \
'" title="' + avatarDescription + '" alt="' + \
avatarDescription + '" class="title">'
profileHeaderStr += ' <h1>' + displayName + '</h1>'
avatarDescription + '" class="title">\n'
profileHeaderStr += ' <h1>' + displayName + '</h1>\n'
iconsDir = getIconsDir(baseDir)
profileHeaderStr += \
'<p><b>@' + nickname + '@' + domainFull + '</b><br>'
@ -3429,17 +3454,19 @@ def htmlProfile(defaultTimeline: str,
'<a href="/users/' + nickname + \
'/qrcode.png" alt="' + translate['QR Code'] + '" title="' + \
translate['QR Code'] + '">' + \
'<img class="qrcode" src="/' + iconsDir + '/qrcode.png" /></a></p>'
profileHeaderStr += ' <p>' + profileDescriptionShort + '</p>'
'<img class="qrcode" src="/' + iconsDir + \
'/qrcode.png" /></a></p>\n'
profileHeaderStr += ' <p>' + profileDescriptionShort + '</p>\n'
profileHeaderStr += loginButton
profileHeaderStr += ' </div>'
profileHeaderStr += '</div>'
profileHeaderStr += ' </div>\n'
profileHeaderStr += '</div>\n'
profileStr = \
linkToTimelineStart + profileHeaderStr + \
linkToTimelineEnd + donateSection
profileStr += '<div class="container" id="buttonheader">\n'
profileStr += ' <center>'
if not isSystemAccount(nickname):
profileStr += \
' <a href="' + usersPath + '#buttonheader"><button class="' + \
postsButton + '"><span>' + translate['Posts'] + \
@ -3454,7 +3481,8 @@ def htmlProfile(defaultTimeline: str,
'"><span>' + translate['Followers'] + ' </span></button></a>'
profileStr += \
' <a href="' + usersPath + '/roles#buttonheader">' + \
'<button class="' + rolesButton + '"><span>' + translate['Roles'] + \
'<button class="' + rolesButton + '"><span>' + \
translate['Roles'] + \
' </span></button></a>'
profileStr += \
' <a href="' + usersPath + '/skills#buttonheader">' + \
@ -3477,6 +3505,12 @@ def htmlProfile(defaultTimeline: str,
profileStyle = \
cssFile.read().replace('image.png',
profileJson['image']['url'])
if isSystemAccount(nickname):
bannerFile, bannerFilename = \
getBannerFile(baseDir, nickname, domain)
profileStyle = \
profileStyle.replace('banner.png',
'/users/' + nickname + '/' + bannerFile)
licenseStr = \
'<a href="https://gitlab.com/bashrc2/epicyon">' + \
@ -3522,8 +3556,27 @@ def htmlProfile(defaultTimeline: str,
htmlProfileShares(actor, translate,
nickname, domainFull,
extraJson) + licenseStr
# Footer which is only used for system accounts
profileFooterStr = ''
if isSystemAccount(nickname):
profileFooterStr = ' </td>\n'
profileFooterStr += ' <td valign="top" class="col-right">\n'
iconsDir = getIconsDir(baseDir)
profileFooterStr += \
getRightColumnContent(baseDir, 'news', domainFull,
httpPrefix, translate,
iconsDir, False, False,
newswire, False,
False, None, False)
profileFooterStr += ' </td>\n'
profileFooterStr += ' </tr>\n'
profileFooterStr += ' </tbody>\n'
profileFooterStr += '</table>\n'
profileStr = \
htmlHeader(cssFilename, profileStyle) + profileStr + htmlFooter()
htmlHeader(cssFilename, profileStyle) + \
profileStr + profileFooterStr + htmlFooter()
return profileStr
@ -4426,13 +4479,21 @@ def individualPostAsHtml(allowDownloads: bool,
if timeDiff > 100:
print('TIMING INDIV ' + boxName + ' 7 = ' + str(timeDiff))
if '/users/news/' not in avatarUrl:
avatarLink = ' <a class="imageAnchor" href="' + postActor + '">'
avatarLink += \
' <img loading="lazy" src="' + avatarUrl + '" title="' + \
translate['Show profile'] + '" alt=" "' + avatarPosition + '/></a>\n'
translate['Show profile'] + '" alt=" "' + avatarPosition + \
'/></a>\n'
else:
avatarLink += \
' <img loading="lazy" src="' + avatarUrl + '" title="' + \
translate['Show profile'] + '" alt=" "' + avatarPosition + \
'/>\n'
if showAvatarOptions and \
fullDomain + '/users/' + nickname not in postActor:
if '/users/news/' not in avatarUrl:
avatarLink = \
' <a class="imageAnchor" href="/users/' + \
nickname + '?options=' + postActor + \
@ -4441,6 +4502,12 @@ def individualPostAsHtml(allowDownloads: bool,
' <img loading="lazy" title="' + \
translate['Show options for this person'] + \
'" src="' + avatarUrl + '" ' + avatarPosition + '/></a>\n'
else:
# don't link to the person options for the news account
avatarLink += \
' <img loading="lazy" title="' + \
translate['Show options for this person'] + \
'" src="' + avatarUrl + '" ' + avatarPosition + '/>\n'
avatarImageInPost = \
' <div class="timeline-avatar">' + avatarLink.strip() + '</div>\n'
@ -4929,13 +4996,17 @@ def individualPostAsHtml(allowDownloads: bool,
if announceAvatarUrl:
idx = 'Show options for this person'
if '/users/news/' not in announceAvatarUrl:
replyAvatarImageInPost = \
' ' \
'<div class="timeline-avatar-reply">\n' \
' <a class="imageAnchor" ' + \
'<div class=' + \
'"timeline-avatar-reply">\n' \
' ' + \
'<a class="imageAnchor" ' + \
'href="/users/' + nickname + \
'?options=' + \
announceActor + ';' + str(pageNumber) + \
announceActor + ';' + \
str(pageNumber) + \
';' + announceAvatarUrl + \
messageIdStr + '">' \
'<img loading="lazy" src="' + \
@ -5437,10 +5508,15 @@ def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str,
iconsDir + '/edit.png" /></a>\n'
# RSS icon
if nickname != 'news':
# rss feed for this account
rssUrl = httpPrefix + '://' + domainFull + \
'/blog/' + nickname + '/rss.xml'
else:
# rss feed for all accounts on the instance
rssUrl = httpPrefix + '://' + domainFull + '/blog/rss.xml'
htmlStr += \
' <a href="' + \
httpPrefix + '://' + domainFull + \
'/blog/' + nickname + '/rss.xml">' + \
' <a href="' + rssUrl + '">' + \
'<img class="' + editImageClass + \
'" loading="lazy" alt="' + \
translate['RSS feed for this site'] + \
@ -5513,7 +5589,7 @@ def votesIndicator(totalVotes: int, positiveVoting: bool) -> str:
return totalVotesStr
def htmlNewswire(newswire: str, nickname: str, moderator: bool,
def htmlNewswire(newswire: {}, nickname: str, moderator: bool,
translate: {}, positiveVoting: bool, iconsDir: str) -> str:
"""Converts a newswire dict into html
"""
@ -5521,7 +5597,7 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool,
for dateStr, item in newswire.items():
publishedDate = \
datetime.strptime(dateStr, "%Y-%m-%d %H:%M:%S+00:00")
dateShown = publishedDate.strftime("%Y-%m-%d")
dateShown = publishedDate.strftime("%Y-%m-%d %H:%M")
dateStrLink = dateStr.replace('T', ' ')
dateStrLink = dateStrLink.replace('Z', '')
@ -5584,7 +5660,8 @@ def getRightColumnContent(baseDir: str, nickname: str, domainFull: str,
httpPrefix: str, translate: {},
iconsDir: str, moderator: bool, editor: bool,
newswire: {}, positiveVoting: bool,
showBackButton: bool, timelinePath: str) -> str:
showBackButton: bool, timelinePath: str,
showPublishButton: bool) -> str:
"""Returns html content for the right column
"""
htmlStr = ''
@ -5627,6 +5704,14 @@ def getRightColumnContent(baseDir: str, nickname: str, domainFull: str,
'<button class="cancelbtn">' + \
translate['Go Back'] + '</button></a>\n'
if showPublishButton:
htmlStr += \
' <a href="' + \
'/users/' + nickname + '/newblog" ' + \
'title="' + translate['Publish a news article'] + '">' + \
'<button class="publishbtn">' + \
translate['Publish'] + '</button></a>\n'
if editor:
if os.path.isfile(baseDir + '/accounts/newswiremoderation.txt'):
# show the edit icon highlighted
@ -5744,11 +5829,36 @@ def htmlNewswireMobile(baseDir: str, nickname: str,
httpPrefix, translate,
iconsDir, moderator, editor,
newswire, positiveVoting,
True, timelinePath)
True, timelinePath, True)
htmlStr += htmlFooter()
return htmlStr
def getBannerFile(baseDir: str, nickname: str, domain: str) -> (str, str):
"""
returns the banner filename
"""
# filename of the banner shown at the top
bannerFile = 'banner.png'
bannerFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + bannerFile
if not os.path.isfile(bannerFilename):
bannerFile = 'banner.jpg'
bannerFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + bannerFile
if not os.path.isfile(bannerFilename):
bannerFile = 'banner.gif'
bannerFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + bannerFile
if not os.path.isfile(bannerFilename):
bannerFile = 'banner.avif'
bannerFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + bannerFile
if not os.path.isfile(bannerFilename):
bannerFile = 'banner.webp'
return bannerFile, bannerFilename
def htmlTimeline(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int,
@ -5824,23 +5934,7 @@ def htmlTimeline(defaultTimeline: str,
cssFilename = baseDir + '/epicyon.css'
# filename of the banner shown at the top
bannerFile = 'banner.png'
bannerFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + bannerFile
if not os.path.isfile(bannerFilename):
bannerFile = 'banner.jpg'
bannerFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + bannerFile
if not os.path.isfile(bannerFilename):
bannerFile = 'banner.gif'
bannerFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + bannerFile
if not os.path.isfile(bannerFilename):
bannerFile = 'banner.avif'
bannerFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + bannerFile
if not os.path.isfile(bannerFilename):
bannerFile = 'banner.webp'
bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain)
# benchmark 1
timeDiff = int((time.time() - timelineStartTime) * 1000)
@ -6136,7 +6230,7 @@ def htmlTimeline(defaultTimeline: str,
# typically the blogs button
# but may change if this is a blogging oriented instance
if defaultTimeline != 'tlblogs':
if not minimal:
if not minimal or defaultTimeline == 'tlnews':
tlStr += \
' <a href="' + usersPath + \
'/tlblogs"><button class="' + \
@ -6422,7 +6516,7 @@ def htmlTimeline(defaultTimeline: str,
httpPrefix, translate, iconsDir,
moderator, editor,
newswire, positiveVoting,
False, None)
False, None, True)
tlStr += ' <td valign="top" class="col-right">' + \
rightColumnStr + ' </td>\n'
tlStr += ' </tr>\n'
@ -7190,7 +7284,8 @@ def htmlUnfollowConfirm(translate: {}, baseDir: str,
def htmlPersonOptions(translate: {}, baseDir: str,
domain: str, originPathStr: str,
domain: str, domainFull: str,
originPathStr: str,
optionsActor: str,
optionsProfileUrl: str,
optionsLink: str,
@ -7328,24 +7423,37 @@ def htmlPersonOptions(translate: {}, baseDir: str,
'name="submitPetname">' + \
translate['Submit'] + '</button><br>\n'
# checkbox for receiving calendar events
if isFollowingActor(baseDir, nickname, domain, optionsActor):
if receivingCalendarEvents(baseDir, nickname, domain,
optionsNickname, optionsDomainFull):
optionsStr += \
checkboxStr = \
' <input type="checkbox" ' + \
'class="profilecheckbox" name="onCalendar" checked> ' + \
translate['Receive calendar events from this account'] + \
'\n <button type="submit" class="buttonsmall" ' + \
'name="submitOnCalendar">' + \
translate['Submit'] + '</button><br>\n'
else:
optionsStr += \
if not receivingCalendarEvents(baseDir, nickname, domain,
optionsNickname, optionsDomainFull):
checkboxStr = checkboxStr.replace(' checked>', '>')
optionsStr += checkboxStr
# checkbox for permission to post to newswire
if optionsDomainFull == domainFull:
if isModerator(baseDir, nickname) and \
not isModerator(baseDir, optionsNickname):
newswireBlockedFilename = \
baseDir + '/accounts/' + \
optionsNickname + '@' + optionsDomain + '/.nonewswire'
checkboxStr = \
' <input type="checkbox" ' + \
'class="profilecheckbox" name="onCalendar"> ' + \
translate['Receive calendar events from this account'] + \
'class="profilecheckbox" name="postsToNews" checked> ' + \
translate['Allow news posts'] + \
'\n <button type="submit" class="buttonsmall" ' + \
'name="submitOnCalendar">' + \
'name="submitPostToNews">' + \
translate['Submit'] + '</button><br>\n'
if os.path.isfile(newswireBlockedFilename):
checkboxStr = checkboxStr.replace(' checked>', '>')
optionsStr += checkboxStr
optionsStr += optionsLinkStr
optionsStr += \