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

View File

@ -14,6 +14,32 @@ from utils import fileLastModified
from utils import getLinkPrefixes 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: def htmlReplaceEmailQuote(content: str) -> str:
"""Replaces an email style quote "> Some quote" with html blockquote """Replaces an email style quote "> Some quote" with html blockquote
""" """
@ -44,9 +70,12 @@ def htmlReplaceEmailQuote(content: str) -> str:
newContent += '<p>' + lineStr + '</p>' newContent += '<p>' + lineStr + '</p>'
else: else:
lineStr = lineStr.replace('>&gt; ', '><blockquote>') lineStr = lineStr.replace('>&gt; ', '><blockquote>')
if lineStr.startswith('&gt;'):
lineStr = lineStr.replace('&gt;', '<blockquote>', 1)
else:
lineStr = lineStr.replace('&gt;', '<br>') lineStr = lineStr.replace('&gt;', '<br>')
newContent += '<p>' + lineStr + '</blockquote></p>' newContent += '<p>' + lineStr + '</blockquote></p>'
return newContent return removeQuotesWithinQuotes(newContent)
def htmlReplaceQuoteMarks(content: str) -> str: 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 canRemovePost
from person import personSnooze from person import personSnooze
from person import personUnsnooze from person import personUnsnooze
from posts import isModerator
from posts import mutePost from posts import mutePost
from posts import unmutePost from posts import unmutePost
from posts import createQuestionPost from posts import createQuestionPost
@ -144,6 +145,8 @@ from webinterface import htmlSearchEmojiTextEntry
from webinterface import htmlUnfollowConfirm from webinterface import htmlUnfollowConfirm
from webinterface import htmlProfileAfterSearch from webinterface import htmlProfileAfterSearch
from webinterface import htmlEditProfile from webinterface import htmlEditProfile
from webinterface import htmlEditLinks
from webinterface import htmlEditNewswire
from webinterface import htmlTermsOfService from webinterface import htmlTermsOfService
from webinterface import htmlSkillsSearch from webinterface import htmlSkillsSearch
from webinterface import htmlHistorySearch from webinterface import htmlHistorySearch
@ -200,6 +203,7 @@ from followingCalendar import removePersonFromCalendar
from devices import E2EEdevicesCollection from devices import E2EEdevicesCollection
from devices import E2EEvalidDevice from devices import E2EEvalidDevice
from devices import E2EEaddDevice from devices import E2EEaddDevice
from newswire import getRSSfromDict
import os import os
@ -2689,6 +2693,216 @@ class PubServer(BaseHTTPRequestHandler):
cookie, callingDomain) cookie, callingDomain)
self.server.POSTbusy = False 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, def _profileUpdate(self, callingDomain: str, cookie: str,
authorized: bool, path: str, authorized: bool, path: str,
baseDir: str, httpPrefix: str, baseDir: str, httpPrefix: str,
@ -2765,7 +2979,8 @@ class PubServer(BaseHTTPRequestHandler):
actorChanged = True actorChanged = True
profileMediaTypes = ('avatar', 'image', profileMediaTypes = ('avatar', 'image',
'banner', 'search_banner', 'banner', 'search_banner',
'instanceLogo') 'instanceLogo',
'left_col_image', 'right_col_image')
profileMediaTypesUploaded = {} profileMediaTypesUploaded = {}
for mType in profileMediaTypes: for mType in profileMediaTypes:
if debug: if debug:
@ -2834,8 +3049,7 @@ class PubServer(BaseHTTPRequestHandler):
# extract all of the text fields into a dict # extract all of the text fields into a dict
fields = \ fields = \
extractTextFieldsInPOST(postBytes, boundary, extractTextFieldsInPOST(postBytes, boundary, debug)
debug)
if debug: if debug:
if fields: if fields:
print('DEBUG: profile update text ' + print('DEBUG: profile update text ' +
@ -3205,10 +3419,14 @@ class PubServer(BaseHTTPRequestHandler):
self.server.defaultTimeline = 'inbox' self.server.defaultTimeline = 'inbox'
if fields['mediaInstance'] == 'on': if fields['mediaInstance'] == 'on':
self.server.mediaInstance = True self.server.mediaInstance = True
self.server.blogsInstance = False
self.server.defaultTimeline = 'tlmedia' self.server.defaultTimeline = 'tlmedia'
setConfigParam(baseDir, setConfigParam(baseDir,
"mediaInstance", "mediaInstance",
self.server.mediaInstance) self.server.mediaInstance)
setConfigParam(baseDir,
"blogsInstance",
self.server.blogsInstance)
else: else:
if self.server.mediaInstance: if self.server.mediaInstance:
self.server.mediaInstance = False self.server.mediaInstance = False
@ -3223,10 +3441,14 @@ class PubServer(BaseHTTPRequestHandler):
self.server.defaultTimeline = 'inbox' self.server.defaultTimeline = 'inbox'
if fields['blogsInstance'] == 'on': if fields['blogsInstance'] == 'on':
self.server.blogsInstance = True self.server.blogsInstance = True
self.server.mediaInstance = False
self.server.defaultTimeline = 'tlblogs' self.server.defaultTimeline = 'tlblogs'
setConfigParam(baseDir, setConfigParam(baseDir,
"blogsInstance", "blogsInstance",
self.server.blogsInstance) self.server.blogsInstance)
setConfigParam(baseDir,
"mediaInstance",
self.server.mediaInstance)
else: else:
if self.server.blogsInstance: if self.server.blogsInstance:
self.server.blogsInstance = False self.server.blogsInstance = False
@ -3733,6 +3955,42 @@ class PubServer(BaseHTTPRequestHandler):
path + ' ' + callingDomain) path + ' ' + callingDomain)
self._404() 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, def _getRSS3feed(self, authorized: bool,
callingDomain: str, path: str, callingDomain: str, path: str,
baseDir: str, httpPrefix: str, baseDir: str, httpPrefix: str,
@ -7047,6 +7305,47 @@ class PubServer(BaseHTTPRequestHandler):
self._404() self._404()
return True 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, def _showBackgroundImage(self, callingDomain: str, path: str,
baseDir: str, baseDir: str,
GETstartTime, GETtimings: {}) -> bool: GETstartTime, GETtimings: {}) -> bool:
@ -7307,6 +7606,50 @@ class PubServer(BaseHTTPRequestHandler):
return True return True
return False 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, def _editEvent(self, callingDomain: str, path: str,
httpPrefix: str, domain: str, domainFull: str, httpPrefix: str, domain: str, domainFull: str,
baseDir: str, translate: {}, baseDir: str, translate: {},
@ -7507,6 +7850,18 @@ class PubServer(BaseHTTPRequestHandler):
self._benchmarkGETtimings(GETstartTime, GETtimings, self._benchmarkGETtimings(GETstartTime, GETtimings,
'fonts', 'sharedInbox enabled') '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 # RSS 2.0
if self.path.startswith('/blog/') and \ if self.path.startswith('/blog/') and \
self.path.endswith('/rss.xml'): self.path.endswith('/rss.xml'):
@ -7965,8 +8320,8 @@ class PubServer(BaseHTTPRequestHandler):
'account qrcode done') 'account qrcode done')
# search screen banner image # search screen banner image
if '/users/' in self.path and \ if '/users/' in self.path:
self.path.endswith('/search_banner.png'): if self.path.endswith('/search_banner.png'):
if self._searchScreenBanner(callingDomain, self.path, if self._searchScreenBanner(callingDomain, self.path,
self.server.baseDir, self.server.baseDir,
self.server.domain, self.server.domain,
@ -7974,6 +8329,22 @@ class PubServer(BaseHTTPRequestHandler):
GETstartTime, GETtimings): GETstartTime, GETtimings):
return 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, self._benchmarkGETtimings(GETstartTime, GETtimings,
'account qrcode done', 'account qrcode done',
'search screen banner done') 'search screen banner done')
@ -8601,6 +8972,26 @@ class PubServer(BaseHTTPRequestHandler):
cookie): cookie):
return 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, if self._showNewPost(callingDomain, self.path,
self.server.mediaInstance, self.server.mediaInstance,
self.server.translate, self.server.translate,
@ -10049,6 +10440,26 @@ class PubServer(BaseHTTPRequestHandler):
self.server.i2pDomain, self.server.debug) self.server.i2pDomain, self.server.debug)
return 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) self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 3)
# moderator action buttons # moderator action buttons
@ -10660,6 +11071,9 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
httpd.unitTest = unitTest httpd.unitTest = unitTest
httpd.YTReplacementDomain = YTReplacementDomain httpd.YTReplacementDomain = YTReplacementDomain
# newswire storing rss feeds
httpd.newswire = {}
# This counter is used to update the list of blocked domains in memory. # 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 # It helps to avoid touching the disk and so improves flooding resistance
httpd.blocklistUpdateCtr = 0 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 { :root {
--main-bg-color: #282c37; --main-bg-color: #282c37;
--column-left-color: #282c37;
--link-bg-color: #282c37; --link-bg-color: #282c37;
--dropdown-fg-color: #dddddd; --dropdown-fg-color: #dddddd;
--dropdown-bg-color: #111; --dropdown-bg-color: #111;
@ -12,6 +13,7 @@
--main-bg-color-report: #221c27; --main-bg-color-report: #221c27;
--main-header-color-roles: #282237; --main-header-color-roles: #282237;
--main-fg-color: #dddddd; --main-fg-color: #dddddd;
--column-left-fg-color: #dddddd;
--main-link-color: #999; --main-link-color: #999;
--main-link-color-hover: #bbb; --main-link-color-hover: #bbb;
--main-visited-color: #888; --main-visited-color: #888;
@ -21,6 +23,7 @@
--font-size-header-mobile: 32px; --font-size-header-mobile: 32px;
--font-color-header: #ccc; --font-color-header: #ccc;
--font-size-button-mobile: 34px; --font-size-button-mobile: 34px;
--font-size-links: 18px;
--font-size: 30px; --font-size: 30px;
--font-size2: 24px; --font-size2: 24px;
--font-size3: 38px; --font-size3: 38px;
@ -61,6 +64,14 @@
--quote-font-weight: normal; --quote-font-weight: normal;
--quote-font-size: 120%; --quote-font-size: 120%;
--line-spacing: 130%; --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 { @font-face {
@ -84,9 +95,7 @@ body, html {
height: 100%; height: 100%;
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
max-width: 80%;
min-width: 950px; min-width: 950px;
margin: 0 auto;
font-size: var(--font-size); font-size: var(--font-size);
line-height: var(--line-spacing); line-height: var(--line-spacing);
} }
@ -126,6 +135,15 @@ h1 {
color: var(--title-color); 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 { a, u {
color: var(--main-fg-color); color: var(--main-fg-color);
} }
@ -158,15 +176,6 @@ a:focus {
border: 2px solid var(--focus-color); 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 { .hero-image {
background-image: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 0.5)), url("image.png"); background-image: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 0.5)), url("image.png");
height: 50%; height: 50%;
@ -735,22 +744,22 @@ a, button, input:focus, input[type='button'], input[type='reset'], input[type='s
text-decoration: none; text-decoration: none;
} }
.button-msgScope { .button-msgScope {
display:flex; display: flex;
flex-direction:row; flex-direction: row;
justify-content:center; justify-content: center;
width:100%; width: 100%;
min-height:100%; min-height: 100%;
} }
.button-msgScope button, .button-msgScope div.lined-thin { .button-msgScope button, .button-msgScope div.lined-thin {
align-self:center; align-self: center;
background:transparent; background: transparent;
padding:1rem 1rem; padding: 1rem 1rem;
margin:0 1rem; margin: 0 1rem;
transition:all .5s ease; transition: all .5s ease;
color:var(--dropdown-fg-color); color: var(--dropdown-fg-color);
font-size:2rem; font-size: 2rem;
letter-spacing:1px; letter-spacing: 1px;
outline:none; outline: none;
} }
.btn { .btn {
margin: -3px 0 0 0; margin: -3px 0 0 0;
@ -923,6 +932,101 @@ aside .toggle-inside li {
} }
@media screen and (min-width: 400px) { @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 { .likesCount {
font-size: var(--font-size-likes); font-size: var(--font-size-likes);
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
@ -1372,6 +1476,53 @@ aside .toggle-inside li {
} }
@media screen and (max-width: 1000px) { @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 { .likesCount {
font-size: var(--font-size-likes-mobile); font-size: var(--font-size-likes-mobile);
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;

View File

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

View File

@ -2125,7 +2125,7 @@ def testReplaceEmailQuote():
"<p>Some other text.</p>" "<p>Some other text.</p>"
resultStr = htmlReplaceEmailQuote(testStr) resultStr = htmlReplaceEmailQuote(testStr)
if resultStr != expectedStr: if resultStr != expectedStr:
print('Result: ' + resultStr) print('Result: ' + str(resultStr))
print('Expect: ' + expectedStr) print('Expect: ' + expectedStr)
assert resultStr == expectedStr assert resultStr == expectedStr
@ -2135,7 +2135,26 @@ def testReplaceEmailQuote():
"second line</blockquote></p><p>Some question?</p>" "second line</blockquote></p><p>Some question?</p>"
resultStr = htmlReplaceEmailQuote(testStr) resultStr = htmlReplaceEmailQuote(testStr)
if resultStr != expectedStr: 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) print('Expect: ' + expectedStr)
assert resultStr == expectedStr assert resultStr == expectedStr

View File

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

View File

@ -287,5 +287,14 @@
"Autogenerated Hashtags": "علامات التجزئة المُنشأة تلقائيًا", "Autogenerated Hashtags": "علامات التجزئة المُنشأة تلقائيًا",
"Autogenerated Content Warnings": "تحذيرات المحتوى المُنشأ تلقائيًا", "Autogenerated Content Warnings": "تحذيرات المحتوى المُنشأ تلقائيًا",
"Indymedia": "Indymedia", "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 Hashtags": "Hashtags autogenerats",
"Autogenerated Content Warnings": "Advertiments de contingut autogenerats", "Autogenerated Content Warnings": "Advertiments de contingut autogenerats",
"Indymedia": "Indymedia", "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 Hashtags": "Hashtags awtogeneiddiedig",
"Autogenerated Content Warnings": "Rhybuddion Cynnwys Autogenerated", "Autogenerated Content Warnings": "Rhybuddion Cynnwys Autogenerated",
"Indymedia": "Indymedia", "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 Hashtags": "Automatisch generierte Hashtags",
"Autogenerated Content Warnings": "Warnungen vor automatisch generierten Inhalten", "Autogenerated Content Warnings": "Warnungen vor automatisch generierten Inhalten",
"Indymedia": "Indymedia", "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 Hashtags": "Autogenerated Hashtags",
"Autogenerated Content Warnings": "Autogenerated Content Warnings", "Autogenerated Content Warnings": "Autogenerated Content Warnings",
"Indymedia": "Indymedia", "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 Hashtags": "Hashtags autogenerados",
"Autogenerated Content Warnings": "Advertencias de contenido generado automáticamente", "Autogenerated Content Warnings": "Advertencias de contenido generado automáticamente",
"Indymedia": "Indymedia", "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 Hashtags": "Hashtags générés automatiquement",
"Autogenerated Content Warnings": "Avertissements de contenu générés automatiquement", "Autogenerated Content Warnings": "Avertissements de contenu générés automatiquement",
"Indymedia": "Indymedia", "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 Hashtags": "Hashtags uathghinte",
"Autogenerated Content Warnings": "Rabhaidh Ábhar Uathghinte", "Autogenerated Content Warnings": "Rabhaidh Ábhar Uathghinte",
"Indymedia": "Indymedia", "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 Hashtags": "ऑटोजेनरेटेड हैशटैग",
"Autogenerated Content Warnings": "स्वतः प्राप्त सामग्री चेतावनी", "Autogenerated Content Warnings": "स्वतः प्राप्त सामग्री चेतावनी",
"Indymedia": "Indymedia", "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 Hashtags": "Hashtag generati automaticamente",
"Autogenerated Content Warnings": "Avvisi sui contenuti generati automaticamente", "Autogenerated Content Warnings": "Avvisi sui contenuti generati automaticamente",
"Indymedia": "Indymedia", "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 Hashtags": "自動生成されたハッシュタグ",
"Autogenerated Content Warnings": "自動生成されたコンテンツの警告", "Autogenerated Content Warnings": "自動生成されたコンテンツの警告",
"Indymedia": "Indymedia", "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 Hashtags": "Autogenerated Hashtags",
"Autogenerated Content Warnings": "Autogenerated Content Warnings", "Autogenerated Content Warnings": "Autogenerated Content Warnings",
"Indymedia": "Indymedia", "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 Hashtags": "Hashtags autogeradas",
"Autogenerated Content Warnings": "Avisos de conteúdo gerado automaticamente", "Autogenerated Content Warnings": "Avisos de conteúdo gerado automaticamente",
"Indymedia": "Indymedia", "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 Hashtags": "Автоматически сгенерированные хештеги",
"Autogenerated Content Warnings": "Автоматические предупреждения о содержании", "Autogenerated Content Warnings": "Автоматические предупреждения о содержании",
"Indymedia": "Indymedia", "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 Hashtags": "自动生成的标签",
"Autogenerated Content Warnings": "自动生成的内容警告", "Autogenerated Content Warnings": "自动生成的内容警告",
"Indymedia": "Indymedia", "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提要"
} }

File diff suppressed because it is too large Load Diff