Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon into main
|
|
@ -66,7 +66,7 @@ def createReject(baseDir: str, federationList: [],
|
|||
return createAcceptReject(baseDir, federationList,
|
||||
nickname, domain, port,
|
||||
toUrl, ccUrl,
|
||||
httpPrefix, objectJson, None, 'Reject')
|
||||
httpPrefix, objectJson, 'Reject')
|
||||
|
||||
|
||||
def acceptFollow(baseDir: str, domain: str, messageJson: {},
|
||||
|
|
|
|||
49
blog.py
|
|
@ -20,6 +20,8 @@ from utils import getDomainFromActor
|
|||
from utils import locatePost
|
||||
from utils import loadJson
|
||||
from posts import createBlogsTimeline
|
||||
from newswire import rss2Header
|
||||
from newswire import rss2Footer
|
||||
|
||||
|
||||
def noOfBlogReplies(baseDir: str, httpPrefix: str, translate: {},
|
||||
|
|
@ -365,12 +367,12 @@ def htmlBlogPost(authorized: bool,
|
|||
'title="RSS 2.0" src="/' + \
|
||||
iconsDir + '/rss.png" /></a>'
|
||||
|
||||
blogStr += '<a href="' + httpPrefix + '://' + \
|
||||
domainFull + '/blog/' + nickname + '/rss.txt">'
|
||||
blogStr += '<img style="width:3%;min-width:50px" ' + \
|
||||
'loading="lazy" alt="RSS 3.0" ' + \
|
||||
'title="RSS 3.0" src="/' + \
|
||||
iconsDir + '/rss3.png" /></a>'
|
||||
# blogStr += '<a href="' + httpPrefix + '://' + \
|
||||
# domainFull + '/blog/' + nickname + '/rss.txt">'
|
||||
# blogStr += '<img style="width:3%;min-width:50px" ' + \
|
||||
# 'loading="lazy" alt="RSS 3.0" ' + \
|
||||
# 'title="RSS 3.0" src="/' + \
|
||||
# iconsDir + '/rss3.png" /></a>'
|
||||
|
||||
blogStr += '</p>'
|
||||
|
||||
|
|
@ -405,7 +407,7 @@ def htmlBlogPage(authorized: bool, session,
|
|||
timelineJson = createBlogsTimeline(session, baseDir,
|
||||
nickname, domain, port,
|
||||
httpPrefix,
|
||||
noOfItems, False, False,
|
||||
noOfItems, False,
|
||||
pageNumber)
|
||||
|
||||
if not timelineJson:
|
||||
|
|
@ -461,11 +463,11 @@ def htmlBlogPage(authorized: bool, session,
|
|||
'title="RSS 2.0" src="/' + \
|
||||
iconsDir + '/rss.png" /></a>'
|
||||
|
||||
blogStr += '<a href="' + httpPrefix + '://' + \
|
||||
domainFull + '/blog/' + nickname + '/rss.txt">'
|
||||
blogStr += '<img loading="lazy" alt="RSS 3.0" ' + \
|
||||
'title="RSS 3.0" src="/' + \
|
||||
iconsDir + '/rss3.png" /></a>'
|
||||
# blogStr += '<a href="' + httpPrefix + '://' + \
|
||||
# domainFull + '/blog/' + nickname + '/rss.txt">'
|
||||
# blogStr += '<img loading="lazy" alt="RSS 3.0" ' + \
|
||||
# 'title="RSS 3.0" src="/' + \
|
||||
# iconsDir + '/rss3.png" /></a>'
|
||||
|
||||
blogStr += '</p>'
|
||||
|
||||
|
|
@ -473,23 +475,6 @@ def htmlBlogPage(authorized: bool, session,
|
|||
return None
|
||||
|
||||
|
||||
def rss2Header(httpPrefix: str,
|
||||
nickname: str, domainFull: str, translate: {}) -> str:
|
||||
rssStr = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"
|
||||
rssStr += "<rss version=\"2.0\">"
|
||||
rssStr += '<channel>'
|
||||
rssStr += ' <title>' + translate['Blog'] + '</title>'
|
||||
rssStr += ' <link>' + httpPrefix + '://' + domainFull + \
|
||||
'/users/' + nickname + '/rss.xml' + '</link>'
|
||||
return rssStr
|
||||
|
||||
|
||||
def rss2Footer() -> str:
|
||||
rssStr = '</channel>'
|
||||
rssStr += '</rss>'
|
||||
return rssStr
|
||||
|
||||
|
||||
def htmlBlogPageRSS2(authorized: bool, session,
|
||||
baseDir: str, httpPrefix: str, translate: {},
|
||||
nickname: str, domain: str, port: int,
|
||||
|
|
@ -505,7 +490,7 @@ def htmlBlogPageRSS2(authorized: bool, session,
|
|||
if port != 80 and port != 443:
|
||||
domainFull = domain + ':' + str(port)
|
||||
|
||||
blogRSS2 = rss2Header(httpPrefix, nickname, domainFull, translate)
|
||||
blogRSS2 = rss2Header(httpPrefix, nickname, domainFull, 'Blog', translate)
|
||||
|
||||
blogsIndex = baseDir + '/accounts/' + \
|
||||
nickname + '@' + domain + '/tlblogs.index'
|
||||
|
|
@ -515,7 +500,7 @@ def htmlBlogPageRSS2(authorized: bool, session,
|
|||
timelineJson = createBlogsTimeline(session, baseDir,
|
||||
nickname, domain, port,
|
||||
httpPrefix,
|
||||
noOfItems, False, False,
|
||||
noOfItems, False,
|
||||
pageNumber)
|
||||
|
||||
if not timelineJson:
|
||||
|
|
@ -561,7 +546,7 @@ def htmlBlogPageRSS3(authorized: bool, session,
|
|||
timelineJson = createBlogsTimeline(session, baseDir,
|
||||
nickname, domain, port,
|
||||
httpPrefix,
|
||||
noOfItems, False, False,
|
||||
noOfItems, False,
|
||||
pageNumber)
|
||||
|
||||
if not timelineJson:
|
||||
|
|
|
|||
33
content.py
|
|
@ -14,6 +14,32 @@ from utils import fileLastModified
|
|||
from utils import getLinkPrefixes
|
||||
|
||||
|
||||
def removeQuotesWithinQuotes(content: str) -> str:
|
||||
"""Removes any blockquote inside blockquote
|
||||
"""
|
||||
if '<blockquote>' not in content:
|
||||
return content
|
||||
if '</blockquote>' not in content:
|
||||
return content
|
||||
ctr = 1
|
||||
found = True
|
||||
while found:
|
||||
prefix = content.split('<blockquote>', ctr)[0] + '<blockquote>'
|
||||
quotedStr = content.split('<blockquote>', ctr)[1]
|
||||
if '</blockquote>' not in quotedStr:
|
||||
found = False
|
||||
else:
|
||||
endStr = quotedStr.split('</blockquote>')[1]
|
||||
quotedStr = quotedStr.split('</blockquote>')[0]
|
||||
if '<blockquote>' not in endStr:
|
||||
found = False
|
||||
if '<blockquote>' in quotedStr:
|
||||
quotedStr = quotedStr.replace('<blockquote>', '')
|
||||
content = prefix + quotedStr + '</blockquote>' + endStr
|
||||
ctr += 1
|
||||
return content
|
||||
|
||||
|
||||
def htmlReplaceEmailQuote(content: str) -> str:
|
||||
"""Replaces an email style quote "> Some quote" with html blockquote
|
||||
"""
|
||||
|
|
@ -44,9 +70,12 @@ def htmlReplaceEmailQuote(content: str) -> str:
|
|||
newContent += '<p>' + lineStr + '</p>'
|
||||
else:
|
||||
lineStr = lineStr.replace('>> ', '><blockquote>')
|
||||
lineStr = lineStr.replace('>', '<br>')
|
||||
if lineStr.startswith('>'):
|
||||
lineStr = lineStr.replace('>', '<blockquote>', 1)
|
||||
else:
|
||||
lineStr = lineStr.replace('>', '<br>')
|
||||
newContent += '<p>' + lineStr + '</blockquote></p>'
|
||||
return newContent
|
||||
return removeQuotesWithinQuotes(newContent)
|
||||
|
||||
|
||||
def htmlReplaceQuoteMarks(content: str) -> str:
|
||||
|
|
|
|||
436
daemon.py
|
|
@ -61,6 +61,7 @@ from person import removeAccount
|
|||
from person import canRemovePost
|
||||
from person import personSnooze
|
||||
from person import personUnsnooze
|
||||
from posts import isModerator
|
||||
from posts import mutePost
|
||||
from posts import unmutePost
|
||||
from posts import createQuestionPost
|
||||
|
|
@ -144,6 +145,8 @@ from webinterface import htmlSearchEmojiTextEntry
|
|||
from webinterface import htmlUnfollowConfirm
|
||||
from webinterface import htmlProfileAfterSearch
|
||||
from webinterface import htmlEditProfile
|
||||
from webinterface import htmlEditLinks
|
||||
from webinterface import htmlEditNewswire
|
||||
from webinterface import htmlTermsOfService
|
||||
from webinterface import htmlSkillsSearch
|
||||
from webinterface import htmlHistorySearch
|
||||
|
|
@ -200,6 +203,7 @@ from followingCalendar import removePersonFromCalendar
|
|||
from devices import E2EEdevicesCollection
|
||||
from devices import E2EEvalidDevice
|
||||
from devices import E2EEaddDevice
|
||||
from newswire import getRSSfromDict
|
||||
import os
|
||||
|
||||
|
||||
|
|
@ -2689,6 +2693,216 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
cookie, callingDomain)
|
||||
self.server.POSTbusy = False
|
||||
|
||||
def _linksUpdate(self, callingDomain: str, cookie: str,
|
||||
authorized: bool, path: str,
|
||||
baseDir: str, httpPrefix: str,
|
||||
domain: str, domainFull: str,
|
||||
onionDomain: str, i2pDomain: str, debug: bool,
|
||||
defaultTimeline: str):
|
||||
"""Updates the left links column of the timeline
|
||||
"""
|
||||
usersPath = path.replace('/linksdata', '')
|
||||
usersPath = usersPath.replace('/editlinks', '')
|
||||
actorStr = httpPrefix + '://' + domainFull + usersPath
|
||||
if ' boundary=' in self.headers['Content-type']:
|
||||
boundary = self.headers['Content-type'].split('boundary=')[1]
|
||||
if ';' in boundary:
|
||||
boundary = boundary.split(';')[0]
|
||||
|
||||
# get the nickname
|
||||
nickname = getNicknameFromActor(actorStr)
|
||||
moderator = None
|
||||
if nickname:
|
||||
moderator = isModerator(baseDir, nickname)
|
||||
if not nickname or not moderator:
|
||||
if callingDomain.endswith('.onion') and \
|
||||
onionDomain:
|
||||
actorStr = \
|
||||
'http://' + onionDomain + usersPath
|
||||
elif (callingDomain.endswith('.i2p') and
|
||||
i2pDomain):
|
||||
actorStr = \
|
||||
'http://' + i2pDomain + usersPath
|
||||
if not nickname:
|
||||
print('WARN: nickname not found in ' + actorStr)
|
||||
else:
|
||||
print('WARN: nickname is not a moderator' + actorStr)
|
||||
self._redirect_headers(actorStr, cookie, callingDomain)
|
||||
self.server.POSTbusy = False
|
||||
return
|
||||
|
||||
length = int(self.headers['Content-length'])
|
||||
|
||||
# check that the POST isn't too large
|
||||
if length > self.server.maxPostLength:
|
||||
if callingDomain.endswith('.onion') and \
|
||||
onionDomain:
|
||||
actorStr = \
|
||||
'http://' + onionDomain + usersPath
|
||||
elif (callingDomain.endswith('.i2p') and
|
||||
i2pDomain):
|
||||
actorStr = \
|
||||
'http://' + i2pDomain + usersPath
|
||||
print('Maximum links data length exceeded ' + str(length))
|
||||
self._redirect_headers(actorStr, cookie, callingDomain)
|
||||
self.server.POSTbusy = False
|
||||
return
|
||||
|
||||
try:
|
||||
# read the bytes of the http form POST
|
||||
postBytes = self.rfile.read(length)
|
||||
except SocketError as e:
|
||||
if e.errno == errno.ECONNRESET:
|
||||
print('WARN: connection was reset while ' +
|
||||
'reading bytes from http form POST')
|
||||
else:
|
||||
print('WARN: error while reading bytes ' +
|
||||
'from http form POST')
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
self.server.POSTbusy = False
|
||||
return
|
||||
except ValueError as e:
|
||||
print('ERROR: failed to read bytes for POST')
|
||||
print(e)
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
self.server.POSTbusy = False
|
||||
return
|
||||
|
||||
linksFilename = baseDir + '/accounts/links.txt'
|
||||
|
||||
# extract all of the text fields into a dict
|
||||
fields = \
|
||||
extractTextFieldsInPOST(postBytes, boundary, debug)
|
||||
if fields.get('editedLinks'):
|
||||
linksStr = fields['editedLinks']
|
||||
linksFile = open(linksFilename, "w+")
|
||||
if linksFile:
|
||||
linksFile.write(linksStr)
|
||||
linksFile.close()
|
||||
else:
|
||||
if os.path.isfile(linksFilename):
|
||||
os.remove(linksFilename)
|
||||
|
||||
# redirect back to the default timeline
|
||||
if callingDomain.endswith('.onion') and \
|
||||
onionDomain:
|
||||
actorStr = \
|
||||
'http://' + onionDomain + usersPath
|
||||
elif (callingDomain.endswith('.i2p') and
|
||||
i2pDomain):
|
||||
actorStr = \
|
||||
'http://' + i2pDomain + usersPath
|
||||
self._redirect_headers(actorStr + '/' + defaultTimeline,
|
||||
cookie, callingDomain)
|
||||
self.server.POSTbusy = False
|
||||
|
||||
def _newswireUpdate(self, callingDomain: str, cookie: str,
|
||||
authorized: bool, path: str,
|
||||
baseDir: str, httpPrefix: str,
|
||||
domain: str, domainFull: str,
|
||||
onionDomain: str, i2pDomain: str, debug: bool,
|
||||
defaultTimeline: str):
|
||||
"""Updates the right newswire column of the timeline
|
||||
"""
|
||||
usersPath = path.replace('/newswiredata', '')
|
||||
usersPath = usersPath.replace('/editnewswire', '')
|
||||
actorStr = httpPrefix + '://' + domainFull + usersPath
|
||||
if ' boundary=' in self.headers['Content-type']:
|
||||
boundary = self.headers['Content-type'].split('boundary=')[1]
|
||||
if ';' in boundary:
|
||||
boundary = boundary.split(';')[0]
|
||||
|
||||
# get the nickname
|
||||
nickname = getNicknameFromActor(actorStr)
|
||||
moderator = None
|
||||
if nickname:
|
||||
moderator = isModerator(baseDir, nickname)
|
||||
if not nickname or not moderator:
|
||||
if callingDomain.endswith('.onion') and \
|
||||
onionDomain:
|
||||
actorStr = \
|
||||
'http://' + onionDomain + usersPath
|
||||
elif (callingDomain.endswith('.i2p') and
|
||||
i2pDomain):
|
||||
actorStr = \
|
||||
'http://' + i2pDomain + usersPath
|
||||
if not nickname:
|
||||
print('WARN: nickname not found in ' + actorStr)
|
||||
else:
|
||||
print('WARN: nickname is not a moderator' + actorStr)
|
||||
self._redirect_headers(actorStr, cookie, callingDomain)
|
||||
self.server.POSTbusy = False
|
||||
return
|
||||
|
||||
length = int(self.headers['Content-length'])
|
||||
|
||||
# check that the POST isn't too large
|
||||
if length > self.server.maxPostLength:
|
||||
if callingDomain.endswith('.onion') and \
|
||||
onionDomain:
|
||||
actorStr = \
|
||||
'http://' + onionDomain + usersPath
|
||||
elif (callingDomain.endswith('.i2p') and
|
||||
i2pDomain):
|
||||
actorStr = \
|
||||
'http://' + i2pDomain + usersPath
|
||||
print('Maximum newswire data length exceeded ' + str(length))
|
||||
self._redirect_headers(actorStr, cookie, callingDomain)
|
||||
self.server.POSTbusy = False
|
||||
return
|
||||
|
||||
try:
|
||||
# read the bytes of the http form POST
|
||||
postBytes = self.rfile.read(length)
|
||||
except SocketError as e:
|
||||
if e.errno == errno.ECONNRESET:
|
||||
print('WARN: connection was reset while ' +
|
||||
'reading bytes from http form POST')
|
||||
else:
|
||||
print('WARN: error while reading bytes ' +
|
||||
'from http form POST')
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
self.server.POSTbusy = False
|
||||
return
|
||||
except ValueError as e:
|
||||
print('ERROR: failed to read bytes for POST')
|
||||
print(e)
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
self.server.POSTbusy = False
|
||||
return
|
||||
|
||||
newswireFilename = baseDir + '/accounts/newswire.txt'
|
||||
|
||||
# extract all of the text fields into a dict
|
||||
fields = \
|
||||
extractTextFieldsInPOST(postBytes, boundary, debug)
|
||||
if fields.get('editedNewswire'):
|
||||
newswireStr = fields['editedNewswire']
|
||||
newswireFile = open(newswireFilename, "w+")
|
||||
if newswireFile:
|
||||
newswireFile.write(newswireStr)
|
||||
newswireFile.close()
|
||||
else:
|
||||
if os.path.isfile(newswireFilename):
|
||||
os.remove(newswireFilename)
|
||||
|
||||
# redirect back to the default timeline
|
||||
if callingDomain.endswith('.onion') and \
|
||||
onionDomain:
|
||||
actorStr = \
|
||||
'http://' + onionDomain + usersPath
|
||||
elif (callingDomain.endswith('.i2p') and
|
||||
i2pDomain):
|
||||
actorStr = \
|
||||
'http://' + i2pDomain + usersPath
|
||||
self._redirect_headers(actorStr + '/' + defaultTimeline,
|
||||
cookie, callingDomain)
|
||||
self.server.POSTbusy = False
|
||||
|
||||
def _profileUpdate(self, callingDomain: str, cookie: str,
|
||||
authorized: bool, path: str,
|
||||
baseDir: str, httpPrefix: str,
|
||||
|
|
@ -2765,7 +2979,8 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
actorChanged = True
|
||||
profileMediaTypes = ('avatar', 'image',
|
||||
'banner', 'search_banner',
|
||||
'instanceLogo')
|
||||
'instanceLogo',
|
||||
'left_col_image', 'right_col_image')
|
||||
profileMediaTypesUploaded = {}
|
||||
for mType in profileMediaTypes:
|
||||
if debug:
|
||||
|
|
@ -2834,8 +3049,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
|
||||
# extract all of the text fields into a dict
|
||||
fields = \
|
||||
extractTextFieldsInPOST(postBytes, boundary,
|
||||
debug)
|
||||
extractTextFieldsInPOST(postBytes, boundary, debug)
|
||||
if debug:
|
||||
if fields:
|
||||
print('DEBUG: profile update text ' +
|
||||
|
|
@ -3205,10 +3419,14 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self.server.defaultTimeline = 'inbox'
|
||||
if fields['mediaInstance'] == 'on':
|
||||
self.server.mediaInstance = True
|
||||
self.server.blogsInstance = False
|
||||
self.server.defaultTimeline = 'tlmedia'
|
||||
setConfigParam(baseDir,
|
||||
"mediaInstance",
|
||||
self.server.mediaInstance)
|
||||
setConfigParam(baseDir,
|
||||
"blogsInstance",
|
||||
self.server.blogsInstance)
|
||||
else:
|
||||
if self.server.mediaInstance:
|
||||
self.server.mediaInstance = False
|
||||
|
|
@ -3223,10 +3441,14 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self.server.defaultTimeline = 'inbox'
|
||||
if fields['blogsInstance'] == 'on':
|
||||
self.server.blogsInstance = True
|
||||
self.server.mediaInstance = False
|
||||
self.server.defaultTimeline = 'tlblogs'
|
||||
setConfigParam(baseDir,
|
||||
"blogsInstance",
|
||||
self.server.blogsInstance)
|
||||
setConfigParam(baseDir,
|
||||
"mediaInstance",
|
||||
self.server.mediaInstance)
|
||||
else:
|
||||
if self.server.blogsInstance:
|
||||
self.server.blogsInstance = False
|
||||
|
|
@ -3733,6 +3955,42 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
path + ' ' + callingDomain)
|
||||
self._404()
|
||||
|
||||
def _getNewswireFeed(self, authorized: bool,
|
||||
callingDomain: str, path: str,
|
||||
baseDir: str, httpPrefix: str,
|
||||
domain: str, port: int, proxyType: str,
|
||||
GETstartTime, GETtimings: {},
|
||||
debug: bool):
|
||||
"""Returns the newswire feed
|
||||
"""
|
||||
if not self.server.session:
|
||||
print('Starting new session during RSS request')
|
||||
self.server.session = \
|
||||
createSession(proxyType)
|
||||
if not self.server.session:
|
||||
print('ERROR: GET failed to create session ' +
|
||||
'during RSS request')
|
||||
self._404()
|
||||
return
|
||||
|
||||
msg = getRSSfromDict(self.server.baseDir, self.server.newswire,
|
||||
self.server.httpPrefix,
|
||||
self.server.domainFull,
|
||||
'Newswire', self.server.translate)
|
||||
if msg:
|
||||
msg = msg.encode('utf-8')
|
||||
self._set_headers('text/xml', len(msg),
|
||||
None, callingDomain)
|
||||
self._write(msg)
|
||||
if debug:
|
||||
print('Sent rss2 newswire feed: ' +
|
||||
path + ' ' + callingDomain)
|
||||
return
|
||||
if debug:
|
||||
print('Failed to get rss2 newswire feed: ' +
|
||||
path + ' ' + callingDomain)
|
||||
self._404()
|
||||
|
||||
def _getRSS3feed(self, authorized: bool,
|
||||
callingDomain: str, path: str,
|
||||
baseDir: str, httpPrefix: str,
|
||||
|
|
@ -7047,6 +7305,47 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self._404()
|
||||
return True
|
||||
|
||||
def _columImage(self, side: str, callingDomain: str, path: str,
|
||||
baseDir: str, domain: str, port: int,
|
||||
GETstartTime, GETtimings: {}) -> bool:
|
||||
"""Shows an image at the top of the left/right column
|
||||
"""
|
||||
nickname = getNicknameFromActor(path)
|
||||
if not nickname:
|
||||
self._404()
|
||||
return True
|
||||
bannerFilename = \
|
||||
baseDir + '/accounts/' + \
|
||||
nickname + '@' + domain + '/' + side + '_col_image.png'
|
||||
if os.path.isfile(bannerFilename):
|
||||
if self._etag_exists(bannerFilename):
|
||||
# The file has not changed
|
||||
self._304()
|
||||
return True
|
||||
|
||||
tries = 0
|
||||
mediaBinary = None
|
||||
while tries < 5:
|
||||
try:
|
||||
with open(bannerFilename, 'rb') as avFile:
|
||||
mediaBinary = avFile.read()
|
||||
break
|
||||
except Exception as e:
|
||||
print(e)
|
||||
time.sleep(1)
|
||||
tries += 1
|
||||
if mediaBinary:
|
||||
self._set_headers_etag(bannerFilename, 'image/png',
|
||||
mediaBinary, None,
|
||||
callingDomain)
|
||||
self._write(mediaBinary)
|
||||
self._benchmarkGETtimings(GETstartTime, GETtimings,
|
||||
'account qrcode done',
|
||||
side + ' col image')
|
||||
return True
|
||||
self._404()
|
||||
return True
|
||||
|
||||
def _showBackgroundImage(self, callingDomain: str, path: str,
|
||||
baseDir: str,
|
||||
GETstartTime, GETtimings: {}) -> bool:
|
||||
|
|
@ -7307,6 +7606,50 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
return True
|
||||
return False
|
||||
|
||||
def _editLinks(self, callingDomain: str, path: str,
|
||||
translate: {}, baseDir: str,
|
||||
httpPrefix: str, domain: str, port: int,
|
||||
cookie: str) -> bool:
|
||||
"""Show the links from the left column
|
||||
"""
|
||||
if '/users/' in path and path.endswith('/editlinks'):
|
||||
msg = htmlEditLinks(translate,
|
||||
baseDir,
|
||||
path, domain,
|
||||
port,
|
||||
httpPrefix).encode('utf-8')
|
||||
if msg:
|
||||
self._set_headers('text/html', len(msg),
|
||||
cookie, callingDomain)
|
||||
self._write(msg)
|
||||
else:
|
||||
self._404()
|
||||
self.server.GETbusy = False
|
||||
return True
|
||||
return False
|
||||
|
||||
def _editNewswire(self, callingDomain: str, path: str,
|
||||
translate: {}, baseDir: str,
|
||||
httpPrefix: str, domain: str, port: int,
|
||||
cookie: str) -> bool:
|
||||
"""Show the newswire from the right column
|
||||
"""
|
||||
if '/users/' in path and path.endswith('/editnewswire'):
|
||||
msg = htmlEditNewswire(translate,
|
||||
baseDir,
|
||||
path, domain,
|
||||
port,
|
||||
httpPrefix).encode('utf-8')
|
||||
if msg:
|
||||
self._set_headers('text/html', len(msg),
|
||||
cookie, callingDomain)
|
||||
self._write(msg)
|
||||
else:
|
||||
self._404()
|
||||
self.server.GETbusy = False
|
||||
return True
|
||||
return False
|
||||
|
||||
def _editEvent(self, callingDomain: str, path: str,
|
||||
httpPrefix: str, domain: str, domainFull: str,
|
||||
baseDir: str, translate: {},
|
||||
|
|
@ -7507,6 +7850,18 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self._benchmarkGETtimings(GETstartTime, GETtimings,
|
||||
'fonts', 'sharedInbox enabled')
|
||||
|
||||
if self.path == '/newswire.xml':
|
||||
self._getNewswireFeed(authorized,
|
||||
callingDomain, self.path,
|
||||
self.server.baseDir,
|
||||
self.server.httpPrefix,
|
||||
self.server.domain,
|
||||
self.server.port,
|
||||
self.server.proxyType,
|
||||
GETstartTime, GETtimings,
|
||||
self.server.debug)
|
||||
return
|
||||
|
||||
# RSS 2.0
|
||||
if self.path.startswith('/blog/') and \
|
||||
self.path.endswith('/rss.xml'):
|
||||
|
|
@ -7965,14 +8320,30 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
'account qrcode done')
|
||||
|
||||
# search screen banner image
|
||||
if '/users/' in self.path and \
|
||||
self.path.endswith('/search_banner.png'):
|
||||
if self._searchScreenBanner(callingDomain, self.path,
|
||||
self.server.baseDir,
|
||||
self.server.domain,
|
||||
self.server.port,
|
||||
GETstartTime, GETtimings):
|
||||
return
|
||||
if '/users/' in self.path:
|
||||
if self.path.endswith('/search_banner.png'):
|
||||
if self._searchScreenBanner(callingDomain, self.path,
|
||||
self.server.baseDir,
|
||||
self.server.domain,
|
||||
self.server.port,
|
||||
GETstartTime, GETtimings):
|
||||
return
|
||||
|
||||
if self.path.endswith('/left_col_image.png'):
|
||||
if self._columImage('left', callingDomain, self.path,
|
||||
self.server.baseDir,
|
||||
self.server.domain,
|
||||
self.server.port,
|
||||
GETstartTime, GETtimings):
|
||||
return
|
||||
|
||||
if self.path.endswith('/right_col_image.png'):
|
||||
if self._columImage('right', callingDomain, self.path,
|
||||
self.server.baseDir,
|
||||
self.server.domain,
|
||||
self.server.port,
|
||||
GETstartTime, GETtimings):
|
||||
return
|
||||
|
||||
self._benchmarkGETtimings(GETstartTime, GETtimings,
|
||||
'account qrcode done',
|
||||
|
|
@ -8601,6 +8972,26 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
cookie):
|
||||
return
|
||||
|
||||
# edit links from the left column of the timeline in web interface
|
||||
if self._editLinks(callingDomain, self.path,
|
||||
self.server.translate,
|
||||
self.server.baseDir,
|
||||
self.server.httpPrefix,
|
||||
self.server.domain,
|
||||
self.server.port,
|
||||
cookie):
|
||||
return
|
||||
|
||||
# edit newswire from the right column of the timeline
|
||||
if self._editNewswire(callingDomain, self.path,
|
||||
self.server.translate,
|
||||
self.server.baseDir,
|
||||
self.server.httpPrefix,
|
||||
self.server.domain,
|
||||
self.server.port,
|
||||
cookie):
|
||||
return
|
||||
|
||||
if self._showNewPost(callingDomain, self.path,
|
||||
self.server.mediaInstance,
|
||||
self.server.translate,
|
||||
|
|
@ -10049,6 +10440,26 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self.server.i2pDomain, self.server.debug)
|
||||
return
|
||||
|
||||
if authorized and self.path.endswith('/linksdata'):
|
||||
self._linksUpdate(callingDomain, cookie, authorized, self.path,
|
||||
self.server.baseDir, self.server.httpPrefix,
|
||||
self.server.domain,
|
||||
self.server.domainFull,
|
||||
self.server.onionDomain,
|
||||
self.server.i2pDomain, self.server.debug,
|
||||
self.server.defaultTimeline)
|
||||
return
|
||||
|
||||
if authorized and self.path.endswith('/newswiredata'):
|
||||
self._newswireUpdate(callingDomain, cookie, authorized, self.path,
|
||||
self.server.baseDir, self.server.httpPrefix,
|
||||
self.server.domain,
|
||||
self.server.domainFull,
|
||||
self.server.onionDomain,
|
||||
self.server.i2pDomain, self.server.debug,
|
||||
self.server.defaultTimeline)
|
||||
return
|
||||
|
||||
self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 3)
|
||||
|
||||
# moderator action buttons
|
||||
|
|
@ -10660,6 +11071,9 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
|
|||
httpd.unitTest = unitTest
|
||||
httpd.YTReplacementDomain = YTReplacementDomain
|
||||
|
||||
# newswire storing rss feeds
|
||||
httpd.newswire = {}
|
||||
|
||||
# This counter is used to update the list of blocked domains in memory.
|
||||
# It helps to avoid touching the disk and so improves flooding resistance
|
||||
httpd.blocklistUpdateCtr = 0
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
:root {
|
||||
--main-bg-color: #282c37;
|
||||
--column-left-color: #282c37;
|
||||
--link-bg-color: #282c37;
|
||||
--dropdown-fg-color: #dddddd;
|
||||
--dropdown-bg-color: #111;
|
||||
|
|
@ -12,6 +13,7 @@
|
|||
--main-bg-color-report: #221c27;
|
||||
--main-header-color-roles: #282237;
|
||||
--main-fg-color: #dddddd;
|
||||
--column-left-fg-color: #dddddd;
|
||||
--main-link-color: #999;
|
||||
--main-link-color-hover: #bbb;
|
||||
--main-visited-color: #888;
|
||||
|
|
@ -21,6 +23,7 @@
|
|||
--font-size-header-mobile: 32px;
|
||||
--font-color-header: #ccc;
|
||||
--font-size-button-mobile: 34px;
|
||||
--font-size-links: 18px;
|
||||
--font-size: 30px;
|
||||
--font-size2: 24px;
|
||||
--font-size3: 38px;
|
||||
|
|
@ -61,6 +64,14 @@
|
|||
--quote-font-weight: normal;
|
||||
--quote-font-size: 120%;
|
||||
--line-spacing: 130%;
|
||||
--column-left-width: 10vw;
|
||||
--column-center-width: 80vw;
|
||||
--column-right-width: 10vw;
|
||||
--column-left-header-background: #555;
|
||||
--column-left-header-color: #fff;
|
||||
--column-left-header-size: 20px;
|
||||
--column-left-icon-size: 20%;
|
||||
--column-right-icon-size: 20%;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
|
@ -84,38 +95,36 @@ body, html {
|
|||
|
||||
height: 100%;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
max-width: 80%;
|
||||
min-width: 950px;
|
||||
margin: 0 auto;
|
||||
font-size: var(--font-size);
|
||||
line-height: var(--line-spacing);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 10px;
|
||||
margin: 1.5em 10px;
|
||||
padding: 0.5em 10px;
|
||||
font-weight: var(--quote-font-weight);
|
||||
font-style: italic;
|
||||
font-size: var(--quote-font-size);
|
||||
quotes: "\201C""\201D""\2018""\2019";
|
||||
border-left: 10px;
|
||||
margin: 1.5em 10px;
|
||||
padding: 0.5em 10px;
|
||||
font-weight: var(--quote-font-weight);
|
||||
font-style: italic;
|
||||
font-size: var(--quote-font-size);
|
||||
quotes: "\201C""\201D""\2018""\2019";
|
||||
}
|
||||
blockquote:before {
|
||||
content: open-quote;
|
||||
font-size: 2em;
|
||||
line-height: 0.1em;
|
||||
margin-right: 0.25em;
|
||||
vertical-align: -0.4em;
|
||||
content: open-quote;
|
||||
font-size: 2em;
|
||||
line-height: 0.1em;
|
||||
margin-right: 0.25em;
|
||||
vertical-align: -0.4em;
|
||||
}
|
||||
blockquote:after {
|
||||
content: close-quote;
|
||||
font-size: 2em;
|
||||
line-height: 0.1em;
|
||||
margin-left: var(--quote-right-margin);
|
||||
vertical-align: -0.4em;
|
||||
content: close-quote;
|
||||
font-size: 2em;
|
||||
line-height: 0.1em;
|
||||
margin-left: var(--quote-right-margin);
|
||||
vertical-align: -0.4em;
|
||||
}
|
||||
blockquote p {
|
||||
display: inline;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.imageAnchor:focus img{
|
||||
|
|
@ -126,6 +135,15 @@ h1 {
|
|||
color: var(--title-color);
|
||||
}
|
||||
|
||||
h3.linksHeader {
|
||||
background-color: var(--column-left-header-background);
|
||||
color: var(--column-left-header-color);
|
||||
font-size: var(--column-left-header-size);
|
||||
text-transform: uppercase;
|
||||
padding: 4px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
a, u {
|
||||
color: var(--main-fg-color);
|
||||
}
|
||||
|
|
@ -158,15 +176,6 @@ a:focus {
|
|||
border: 2px solid var(--focus-color);
|
||||
}
|
||||
|
||||
.timeline-banner {
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 0.5)), url("banner.png");
|
||||
height: 10%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 0.5)), url("image.png");
|
||||
height: 50%;
|
||||
|
|
@ -735,22 +744,22 @@ a, button, input:focus, input[type='button'], input[type='reset'], input[type='s
|
|||
text-decoration: none;
|
||||
}
|
||||
.button-msgScope {
|
||||
display:flex;
|
||||
flex-direction:row;
|
||||
justify-content:center;
|
||||
width:100%;
|
||||
min-height:100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
.button-msgScope button, .button-msgScope div.lined-thin {
|
||||
align-self:center;
|
||||
background:transparent;
|
||||
padding:1rem 1rem;
|
||||
margin:0 1rem;
|
||||
transition:all .5s ease;
|
||||
color:var(--dropdown-fg-color);
|
||||
font-size:2rem;
|
||||
letter-spacing:1px;
|
||||
outline:none;
|
||||
align-self: center;
|
||||
background: transparent;
|
||||
padding: 1rem 1rem;
|
||||
margin: 0 1rem;
|
||||
transition: all .5s ease;
|
||||
color: var(--dropdown-fg-color);
|
||||
font-size: 2rem;
|
||||
letter-spacing: 1px;
|
||||
outline: none;
|
||||
}
|
||||
.btn {
|
||||
margin: -3px 0 0 0;
|
||||
|
|
@ -923,6 +932,101 @@ aside .toggle-inside li {
|
|||
}
|
||||
|
||||
@media screen and (min-width: 400px) {
|
||||
body, html {
|
||||
background-color: var(--main-bg-color);
|
||||
color: var(--main-fg-color);
|
||||
|
||||
height: 100%;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
min-width: 950px;
|
||||
font-size: var(--font-size);
|
||||
line-height: var(--line-spacing);
|
||||
}
|
||||
.timeline-banner {
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 0.5)), url("banner.png");
|
||||
height: 15%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100vw;
|
||||
position: relative;
|
||||
}
|
||||
.timeline {
|
||||
border: 0;
|
||||
width: 100vw;
|
||||
}
|
||||
.col-left a:link {
|
||||
background: var(--column-left-color);
|
||||
}
|
||||
.col-left a:visited {
|
||||
background: var(--column-left-color);
|
||||
}
|
||||
.column-left {
|
||||
background-color: var(--column-left-color);
|
||||
width: var(--column-left-width);
|
||||
}
|
||||
.col-left {
|
||||
color: var(--column-left-fg-color);
|
||||
padding: 10px 10px;
|
||||
font-size: var(--font-size-links);
|
||||
float: left;
|
||||
width: var(--column-left-width);
|
||||
}
|
||||
.col-left img.leftColEdit {
|
||||
background: var(--column-left-color);
|
||||
margin: 40px 0;
|
||||
width: var(--column-left-icon-size);
|
||||
}
|
||||
.col-left img.leftColEditImage {
|
||||
background: var(--column-left-color);
|
||||
width: var(--column-left-icon-size);
|
||||
float: right;
|
||||
}
|
||||
.col-left img.leftColImg {
|
||||
background: var(--column-left-color);
|
||||
width: 100%;
|
||||
margin: 0 0;
|
||||
padding: 0 0;
|
||||
}
|
||||
.column-center {
|
||||
width: var(--column-center-width);
|
||||
}
|
||||
.col-center {
|
||||
width: var(--column-center-width);
|
||||
background-color: var(--main-bg-color);
|
||||
}
|
||||
.col-right a:link {
|
||||
background: var(--column-left-color);
|
||||
}
|
||||
.col-right a:visited {
|
||||
background: var(--column-left-color);
|
||||
}
|
||||
.column-right {
|
||||
background-color: var(--column-left-color);
|
||||
width: var(--column-right-width);
|
||||
}
|
||||
.col-right {
|
||||
background-color: var(--column-left-color);
|
||||
color: var(--column-left-fg-color);
|
||||
padding-right: 30px;
|
||||
font-size: var(--font-size-links);
|
||||
width: var(--column-right-width);
|
||||
}
|
||||
.col-right img.rightColEdit {
|
||||
background: var(--column-left-color);
|
||||
margin: 40px 0;
|
||||
width: var(--column-right-icon-size);
|
||||
}
|
||||
.col-right img.rightColEditImage {
|
||||
background: var(--column-left-color);
|
||||
width: var(--column-right-icon-size);
|
||||
float: right;
|
||||
}
|
||||
.col-right img.rightColImg {
|
||||
background: var(--column-left-color);
|
||||
width: 100%;
|
||||
margin: 0 0;
|
||||
padding: 0 0;
|
||||
}
|
||||
.likesCount {
|
||||
font-size: var(--font-size-likes);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
|
|
@ -1372,6 +1476,53 @@ aside .toggle-inside li {
|
|||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
body, html {
|
||||
background-color: var(--main-bg-color);
|
||||
color: var(--main-fg-color);
|
||||
|
||||
height: 100%;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
min-width: 950px;
|
||||
margin-left: 0;
|
||||
font-size: var(--font-size);
|
||||
line-height: var(--line-spacing);
|
||||
}
|
||||
.timeline-banner {
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 0.5)), url("banner.png");
|
||||
height: 6%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 145vw;
|
||||
position: relative;
|
||||
}
|
||||
.timeline {
|
||||
border: 0;
|
||||
width: 100vw;
|
||||
}
|
||||
.column-left {
|
||||
display: none;
|
||||
width: 0%;
|
||||
}
|
||||
.col-left {
|
||||
float: left;
|
||||
width: 0%;
|
||||
display: none;
|
||||
}
|
||||
.col-center {
|
||||
width: 100vw;
|
||||
}
|
||||
.col-right {
|
||||
float: right;
|
||||
width: 0%;
|
||||
display: none;
|
||||
}
|
||||
.column-right {
|
||||
display: none;
|
||||
width: 0%;
|
||||
}
|
||||
.column-center {
|
||||
width: 100%;
|
||||
}
|
||||
.likesCount {
|
||||
font-size: var(--font-size-likes-mobile);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
|
|
|
|||
13
epicyon.py
|
|
@ -28,6 +28,7 @@ from posts import getUserUrl
|
|||
from posts import checkDomains
|
||||
from session import createSession
|
||||
from session import getJson
|
||||
from newswire import getRSS
|
||||
from filters import addFilter
|
||||
from filters import removeFilter
|
||||
import os
|
||||
|
|
@ -176,6 +177,8 @@ parser.add_argument('--postsraw', dest='postsraw', type=str,
|
|||
help='Show raw json of posts for the given handle')
|
||||
parser.add_argument('--json', dest='json', type=str, default=None,
|
||||
help='Show the json for a given activitypub url')
|
||||
parser.add_argument('--rss', dest='rss', type=str, default=None,
|
||||
help='Show an rss feed for a given url')
|
||||
parser.add_argument('-f', '--federate', nargs='+', dest='federationList',
|
||||
help='Specify federation list separated by spaces')
|
||||
parser.add_argument("--repliesEnabled", "--commentsEnabled",
|
||||
|
|
@ -595,6 +598,12 @@ if args.json:
|
|||
pprint(testJson)
|
||||
sys.exit()
|
||||
|
||||
if args.rss:
|
||||
session = createSession(None)
|
||||
testRSS = getRSS(session, args.rss)
|
||||
pprint(testRSS)
|
||||
sys.exit()
|
||||
|
||||
# create cache for actors
|
||||
if not os.path.isdir(baseDir + '/cache'):
|
||||
os.mkdir(baseDir + '/cache')
|
||||
|
|
@ -615,11 +624,15 @@ if not args.mediainstance:
|
|||
mediaInstance = getConfigParam(baseDir, 'mediaInstance')
|
||||
if mediaInstance is not None:
|
||||
args.mediainstance = mediaInstance
|
||||
if args.mediainstance:
|
||||
args.blogsinstance = False
|
||||
|
||||
if not args.blogsinstance:
|
||||
blogsInstance = getConfigParam(baseDir, 'blogsInstance')
|
||||
if blogsInstance is not None:
|
||||
args.blogsinstance = blogsInstance
|
||||
if args.blogsinstance:
|
||||
args.mediainstance = False
|
||||
|
||||
# set the instance title in config.json
|
||||
title = getConfigParam(baseDir, 'instanceTitle')
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 29 KiB |
|
|
@ -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
|
||||
|
|
@ -54,7 +54,7 @@ def createSession(proxyType: str):
|
|||
|
||||
|
||||
def getJson(session, url: str, headers: {}, params: {},
|
||||
version='1.0.0', httpPrefix='https',
|
||||
version='1.1.0', httpPrefix='https',
|
||||
domain='testdomain') -> {}:
|
||||
if not isinstance(url, str):
|
||||
print('url: ' + str(url))
|
||||
|
|
|
|||
23
tests.py
|
|
@ -2125,7 +2125,7 @@ def testReplaceEmailQuote():
|
|||
"<p>Some other text.</p>"
|
||||
resultStr = htmlReplaceEmailQuote(testStr)
|
||||
if resultStr != expectedStr:
|
||||
print('Result: ' + resultStr)
|
||||
print('Result: ' + str(resultStr))
|
||||
print('Expect: ' + expectedStr)
|
||||
assert resultStr == expectedStr
|
||||
|
||||
|
|
@ -2135,7 +2135,26 @@ def testReplaceEmailQuote():
|
|||
"second line</blockquote></p><p>Some question?</p>"
|
||||
resultStr = htmlReplaceEmailQuote(testStr)
|
||||
if resultStr != expectedStr:
|
||||
print('Result: ' + resultStr)
|
||||
print('Result: ' + str(resultStr))
|
||||
print('Expect: ' + expectedStr)
|
||||
assert resultStr == expectedStr
|
||||
|
||||
testStr = "<p><span class=\"h-card\">" + \
|
||||
"<a href=\"https://somedomain/@somenick\" " + \
|
||||
"class=\"u-url mention\">@<span>somenick</span>" + \
|
||||
"</a></span> </p><p>> Text1.<br />> <br />" + \
|
||||
"> Text2<br />> <br />> Text3<br />" + \
|
||||
"><br />> Text4<br />> <br />> " + \
|
||||
"Text5<br />> <br />> 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 />><br />Text4<br />" + \
|
||||
"<br />Text5<br /><br />Text6</blockquote></p><p>Text7</p>"
|
||||
resultStr = htmlReplaceEmailQuote(testStr)
|
||||
if resultStr != expectedStr:
|
||||
print('Result: ' + str(resultStr))
|
||||
print('Expect: ' + expectedStr)
|
||||
assert resultStr == expectedStr
|
||||
|
||||
|
|
|
|||
60
theme.py
|
|
@ -15,7 +15,7 @@ from shutil import copyfile
|
|||
def getThemeFiles() -> []:
|
||||
return ('epicyon.css', 'login.css', 'follow.css',
|
||||
'suspended.css', 'calendar.css', 'blog.css',
|
||||
'options.css', 'search.css')
|
||||
'options.css', 'search.css', 'links.css')
|
||||
|
||||
|
||||
def getThemesList() -> []:
|
||||
|
|
@ -264,12 +264,17 @@ def setThemeIndymedia(baseDir: str):
|
|||
"font-size4": "24px",
|
||||
"font-size5": "22px",
|
||||
"main-bg-color": "black",
|
||||
"column-left-header-color": "#fff",
|
||||
"column-left-header-background": "#555",
|
||||
"column-left-header-size": "20px",
|
||||
"column-left-color": "#003366",
|
||||
"text-entry-background": "#0f0d10",
|
||||
"link-bg-color": "black",
|
||||
"main-link-color": "#ff9900",
|
||||
"main-link-color-hover": "#d09338",
|
||||
"main-visited-color": "#ffb900",
|
||||
"main-fg-color": "white",
|
||||
"column-left-fg-color": "white",
|
||||
"main-bg-color-dm": "#0b0a0a",
|
||||
"border-color": "#003366",
|
||||
"border-width": "0",
|
||||
|
|
@ -292,6 +297,10 @@ def setThemeIndymedia(baseDir: str):
|
|||
"title-text": "white",
|
||||
"title-background": "#003366",
|
||||
"quote-right-margin": "0.1em",
|
||||
"column-left-width": "10vw",
|
||||
"column-center-width": "70vw",
|
||||
"column-right-width": "20vw",
|
||||
"column-right-icon-size": "11%"
|
||||
}
|
||||
setThemeFromDict(baseDir, name, themeParams, bgParams)
|
||||
|
||||
|
|
@ -311,6 +320,7 @@ def setThemeBlue(baseDir: str):
|
|||
"gallery-font-size": "35px",
|
||||
"gallery-font-size-mobile": "55px",
|
||||
"main-bg-color": "#002365",
|
||||
"column-left-color": "#002365",
|
||||
"text-entry-background": "#002365",
|
||||
"link-bg-color": "#002365",
|
||||
"main-bg-color-reply": "#002365",
|
||||
|
|
@ -348,11 +358,13 @@ def setThemeNight(baseDir: str):
|
|||
"font-size4": "24px",
|
||||
"font-size5": "22px",
|
||||
"main-bg-color": "#0f0d10",
|
||||
"column-left-color": "#0f0d10",
|
||||
"text-entry-background": "#0f0d10",
|
||||
"link-bg-color": "#0f0d10",
|
||||
"main-link-color": "ff9900",
|
||||
"main-link-color-hover": "#d09338",
|
||||
"main-fg-color": "#a961ab",
|
||||
"column-left-fg-color": "#a961ab",
|
||||
"main-bg-color-dm": "#0b0a0a",
|
||||
"border-color": "#606984",
|
||||
"main-bg-color-reply": "#0f0d10",
|
||||
|
|
@ -398,6 +410,7 @@ def setThemeStarlight(baseDir: str):
|
|||
"font-size4": "24px",
|
||||
"font-size5": "22px",
|
||||
"main-bg-color": "#0f0d10",
|
||||
"column-left-color": "#0f0d10",
|
||||
"text-entry-background": "#0f0d10",
|
||||
"link-bg-color": "#0f0d10",
|
||||
"main-link-color": "#ffc4bc",
|
||||
|
|
@ -405,6 +418,7 @@ def setThemeStarlight(baseDir: str):
|
|||
"title-color": "#ffc4bc",
|
||||
"main-visited-color": "#e1c4bc",
|
||||
"main-fg-color": "#ffc4bc",
|
||||
"column-left-fg-color": "#ffc4bc",
|
||||
"main-bg-color-dm": "#0b0a0a",
|
||||
"border-color": "#69282c",
|
||||
"border-width": "3px",
|
||||
|
|
@ -457,6 +471,7 @@ def setThemeHenge(baseDir: str):
|
|||
"font-size4": "24px",
|
||||
"font-size5": "22px",
|
||||
"main-bg-color": "#383335",
|
||||
"column-left-color": "#383335",
|
||||
"text-entry-background": "#383335",
|
||||
"link-bg-color": "#383335",
|
||||
"main-link-color": "white",
|
||||
|
|
@ -464,6 +479,7 @@ def setThemeHenge(baseDir: str):
|
|||
"title-color": "white",
|
||||
"main-visited-color": "#e1c4bc",
|
||||
"main-fg-color": "white",
|
||||
"column-left-fg-color": "white",
|
||||
"main-bg-color-dm": "#343335",
|
||||
"border-color": "#222",
|
||||
"border-width": "5px",
|
||||
|
|
@ -506,6 +522,7 @@ def setThemeZen(baseDir: str):
|
|||
setThemeInConfig(baseDir, name)
|
||||
themeParams = {
|
||||
"main-bg-color": "#5c4e41",
|
||||
"column-left-color": "#5c4e41",
|
||||
"text-entry-background": "#5c4e41",
|
||||
"link-bg-color": "#5c4e41",
|
||||
"main-bg-color-reply": "#5c4e41",
|
||||
|
|
@ -565,6 +582,7 @@ def setThemeLCD(baseDir: str):
|
|||
name = 'lcd'
|
||||
themeParams = {
|
||||
"main-bg-color": "#9fb42b",
|
||||
"column-left-color": "#9fb42b",
|
||||
"link-bg-color": "#33390d",
|
||||
"text-entry-foreground": "#33390d",
|
||||
"text-entry-background": "#9fb42b",
|
||||
|
|
@ -573,6 +591,7 @@ def setThemeLCD(baseDir: str):
|
|||
"main-bg-color-dm": "#5fb42b",
|
||||
"main-header-color-roles": "#9fb42b",
|
||||
"main-fg-color": "#33390d",
|
||||
"column-left-fg-color": "#33390d",
|
||||
"border-color": "#33390d",
|
||||
"border-width": "5px",
|
||||
"main-link-color": "#9fb42b",
|
||||
|
|
@ -641,11 +660,13 @@ def setThemePurple(baseDir: str):
|
|||
"font-size4": "24px",
|
||||
"font-size5": "22px",
|
||||
"main-bg-color": "#1f152d",
|
||||
"column-left-color": "#1f152d",
|
||||
"link-bg-color": "#1f152d",
|
||||
"main-bg-color-reply": "#1a142d",
|
||||
"main-bg-color-report": "#12152d",
|
||||
"main-header-color-roles": "#1f192d",
|
||||
"main-fg-color": "#f98bb0",
|
||||
"column-left-fg-color": "#f98bb0",
|
||||
"border-color": "#3f2145",
|
||||
"main-link-color": "#ff42a0",
|
||||
"main-link-color-hover": "white",
|
||||
|
|
@ -689,12 +710,14 @@ def setThemeHacker(baseDir: str):
|
|||
themeParams = {
|
||||
"focus-color": "green",
|
||||
"main-bg-color": "black",
|
||||
"column-left-color": "black",
|
||||
"link-bg-color": "black",
|
||||
"main-bg-color-dm": "#0b0a0a",
|
||||
"main-bg-color-reply": "#030202",
|
||||
"main-bg-color-report": "#050202",
|
||||
"main-header-color-roles": "#1f192d",
|
||||
"main-fg-color": "#00ff00",
|
||||
"column-left-fg-color": "#00ff00",
|
||||
"border-color": "#035103",
|
||||
"main-link-color": "#2fff2f",
|
||||
"main-link-color-hover": "#afff2f",
|
||||
|
|
@ -747,6 +770,7 @@ def setThemeLight(baseDir: str):
|
|||
"font-size4": "24px",
|
||||
"font-size5": "22px",
|
||||
"rgba(0, 0, 0, 0.5)": "rgba(0, 0, 0, 0.0)",
|
||||
"column-left-color": "#e6ebf0",
|
||||
"main-bg-color": "#e6ebf0",
|
||||
"main-bg-color-dm": "#e3dbf0",
|
||||
"link-bg-color": "#e6ebf0",
|
||||
|
|
@ -754,6 +778,7 @@ def setThemeLight(baseDir: str):
|
|||
"main-bg-color-report": "#e3dbf0",
|
||||
"main-header-color-roles": "#ebebf0",
|
||||
"main-fg-color": "#2d2c37",
|
||||
"column-left-fg-color": "#2d2c37",
|
||||
"border-color": "#c0cdd9",
|
||||
"main-link-color": "#2a2c37",
|
||||
"main-link-color-hover": "#aa2c37",
|
||||
|
|
@ -804,12 +829,14 @@ def setThemeSolidaric(baseDir: str):
|
|||
"font-size5": "22px",
|
||||
"rgba(0, 0, 0, 0.5)": "rgba(0, 0, 0, 0.0)",
|
||||
"main-bg-color": "white",
|
||||
"column-left-color": "white",
|
||||
"main-bg-color-dm": "white",
|
||||
"link-bg-color": "white",
|
||||
"main-bg-color-reply": "white",
|
||||
"main-bg-color-report": "white",
|
||||
"main-header-color-roles": "#ebebf0",
|
||||
"main-fg-color": "#2d2c37",
|
||||
"column-left-fg-color": "#2d2c37",
|
||||
"border-color": "#c0cdd9",
|
||||
"main-link-color": "#2a2c37",
|
||||
"main-link-color-hover": "#aa2c37",
|
||||
|
|
@ -864,6 +891,10 @@ def setThemeImages(baseDir: str, name: str) -> None:
|
|||
baseDir + '/img/banner.png'
|
||||
searchBannerFilename = \
|
||||
baseDir + '/img/search_banner.png'
|
||||
leftColImageFilename = \
|
||||
baseDir + '/img/left_col_image.png'
|
||||
rightColImageFilename = \
|
||||
baseDir + '/img/right_col_image.png'
|
||||
else:
|
||||
profileImageFilename = \
|
||||
baseDir + '/img/image_' + themeNameLower + '.png'
|
||||
|
|
@ -871,6 +902,10 @@ def setThemeImages(baseDir: str, name: str) -> None:
|
|||
baseDir + '/img/banner_' + themeNameLower + '.png'
|
||||
searchBannerFilename = \
|
||||
baseDir + '/img/search_banner_' + themeNameLower + '.png'
|
||||
leftColImageFilename = \
|
||||
baseDir + '/img/left_col_image_' + themeNameLower + '.png'
|
||||
rightColImageFilename = \
|
||||
baseDir + '/img/right_col_image_' + themeNameLower + '.png'
|
||||
|
||||
backgroundNames = ('login', 'shares', 'delete', 'follow',
|
||||
'options', 'block', 'search', 'calendar')
|
||||
|
|
@ -937,6 +972,29 @@ def setThemeImages(baseDir: str, name: str) -> None:
|
|||
except BaseException:
|
||||
pass
|
||||
|
||||
try:
|
||||
if os.path.isfile(leftColImageFilename):
|
||||
copyfile(leftColImageFilename,
|
||||
accountDir + '/left_col_image.png')
|
||||
else:
|
||||
if os.path.isfile(accountDir +
|
||||
'/left_col_image.png'):
|
||||
os.remove(accountDir + '/left_col_image.png')
|
||||
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
try:
|
||||
if os.path.isfile(rightColImageFilename):
|
||||
copyfile(rightColImageFilename,
|
||||
accountDir + '/right_col_image.png')
|
||||
else:
|
||||
if os.path.isfile(accountDir +
|
||||
'/right_col_image.png'):
|
||||
os.remove(accountDir + '/right_col_image.png')
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
|
||||
def setTheme(baseDir: str, name: str) -> bool:
|
||||
result = False
|
||||
|
|
|
|||
|
|
@ -287,5 +287,14 @@
|
|||
"Autogenerated Hashtags": "علامات التجزئة المُنشأة تلقائيًا",
|
||||
"Autogenerated Content Warnings": "تحذيرات المحتوى المُنشأ تلقائيًا",
|
||||
"Indymedia": "Indymedia",
|
||||
"Hashtag Blocked": "Hashtag محظور"
|
||||
"Hashtag Blocked": "Hashtag محظور",
|
||||
"This is a blogging instance": "هذا مثال على المدونات",
|
||||
"Edit Links": "تحرير الارتباطات",
|
||||
"One link per line. Description followed by the link.": "رابط واحد في كل سطر. الوصف متبوع بالرابط.",
|
||||
"Left column image": "صورة العمود الأيسر",
|
||||
"Right column image": "صورة العمود الأيمن",
|
||||
"RSS feed for this site": "تغذية RSS لهذا الموقع",
|
||||
"Edit newswire": "تحرير الأخبار",
|
||||
"Add RSS feed links below.": "إضافة روابط تغذية RSS أدناه.",
|
||||
"Newswire RSS Feed": "Newswire موجز RSS"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,5 +287,14 @@
|
|||
"Autogenerated Hashtags": "Hashtags autogenerats",
|
||||
"Autogenerated Content Warnings": "Advertiments de contingut autogenerats",
|
||||
"Indymedia": "Indymedia",
|
||||
"Hashtag Blocked": "Hashtag bloquejat"
|
||||
"Hashtag Blocked": "Hashtag bloquejat",
|
||||
"This is a blogging instance": "Aquesta és una instància de blocs",
|
||||
"Edit Links": "Edita els enllaços",
|
||||
"One link per line. Description followed by the link.": "Un enllaç per línia. Descripció seguida de l'enllaç.",
|
||||
"Left column image": "Imatge de la columna esquerra",
|
||||
"Right column image": "Imatge de la columna dreta",
|
||||
"RSS feed for this site": "Feed RSS per a aquest lloc",
|
||||
"Edit newswire": "Editeu newswire",
|
||||
"Add RSS feed links below.": "Afegiu enllaços de canals RSS a continuació.",
|
||||
"Newswire RSS Feed": "Feed RSS de Newswire"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,5 +287,14 @@
|
|||
"Autogenerated Hashtags": "Hashtags awtogeneiddiedig",
|
||||
"Autogenerated Content Warnings": "Rhybuddion Cynnwys Autogenerated",
|
||||
"Indymedia": "Indymedia",
|
||||
"Hashtag Blocked": "Hashtag wedi'i Blocio"
|
||||
"Hashtag Blocked": "Hashtag wedi'i Blocio",
|
||||
"This is a blogging instance": "Dyma enghraifft blogio",
|
||||
"Edit Links": "Golygu Dolenni",
|
||||
"One link per line. Description followed by the link.": "Un dolen y llinell. Disgrifiad wedi'i ddilyn gan y ddolen.",
|
||||
"Left column image": "Delwedd colofn chwith",
|
||||
"Right column image": "Delwedd colofn dde",
|
||||
"RSS feed for this site": "Porthiant RSS ar gyfer y wefan hon",
|
||||
"Edit newswire": "Golygu newyddion",
|
||||
"Add RSS feed links below.": "Ychwanegwch ddolenni porthiant RSS isod.",
|
||||
"Newswire RSS Feed": "Newswire RSS Feed"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,5 +287,14 @@
|
|||
"Autogenerated Hashtags": "Automatisch generierte Hashtags",
|
||||
"Autogenerated Content Warnings": "Warnungen vor automatisch generierten Inhalten",
|
||||
"Indymedia": "Indymedia",
|
||||
"Hashtag Blocked": "Hashtag blockiert"
|
||||
"Hashtag Blocked": "Hashtag blockiert",
|
||||
"This is a blogging instance": "Dies ist eine Blogging-Instanz",
|
||||
"Edit Links": "Links bearbeiten",
|
||||
"One link per line. Description followed by the link.": "Ein Link pro Zeile. Beschreibung gefolgt vom Link.",
|
||||
"Left column image": "Bild in der linken Spalte",
|
||||
"Right column image": "Bild in der rechten Spalte",
|
||||
"RSS feed for this site": "RSS-Feed für diese Site",
|
||||
"Edit newswire": "Newswire bearbeiten",
|
||||
"Add RSS feed links below.": "Fügen Sie unten RSS-Feed-Links hinzu.",
|
||||
"Newswire RSS Feed": "Newswire RSS Feed"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,5 +287,14 @@
|
|||
"Autogenerated Hashtags": "Autogenerated Hashtags",
|
||||
"Autogenerated Content Warnings": "Autogenerated Content Warnings",
|
||||
"Indymedia": "Indymedia",
|
||||
"Hashtag Blocked": "Hashtag Blocked"
|
||||
"Hashtag Blocked": "Hashtag Blocked",
|
||||
"This is a blogging instance": "This is a blogging instance",
|
||||
"Edit Links": "Edit Links",
|
||||
"One link per line. Description followed by the link.": "One link per line. Description followed by the link. Titles should begin with #",
|
||||
"Left column image": "Left column image",
|
||||
"Right column image": "Right column image",
|
||||
"RSS feed for this site": "RSS feed for this site",
|
||||
"Edit newswire": "Edit newswire",
|
||||
"Add RSS feed links below.": "Add RSS feed links below.",
|
||||
"Newswire RSS Feed": "Newswire RSS Feed"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,5 +287,14 @@
|
|||
"Autogenerated Hashtags": "Hashtags autogenerados",
|
||||
"Autogenerated Content Warnings": "Advertencias de contenido generado automáticamente",
|
||||
"Indymedia": "Indymedia",
|
||||
"Hashtag Blocked": "Hashtag bloqueada"
|
||||
"Hashtag Blocked": "Hashtag bloqueada",
|
||||
"This is a blogging instance": "Esta es una instancia de blogs",
|
||||
"Edit Links": "Editar enlaces",
|
||||
"One link per line. Description followed by the link.": "Un enlace por línea. Descripción seguida del enlace.",
|
||||
"Left column image": "Imagen de la columna izquierda",
|
||||
"Right column image": "Imagen de la columna derecha",
|
||||
"RSS feed for this site": "Fuente RSS para este sitio",
|
||||
"Edit newswire": "Editar newswire",
|
||||
"Add RSS feed links below.": "Agregue los enlaces de fuentes RSS a continuación.",
|
||||
"Newswire RSS Feed": "Canal RSS de Newswire"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,5 +287,14 @@
|
|||
"Autogenerated Hashtags": "Hashtags générés automatiquement",
|
||||
"Autogenerated Content Warnings": "Avertissements de contenu générés automatiquement",
|
||||
"Indymedia": "Indymedia",
|
||||
"Hashtag Blocked": "Hashtag bloqué"
|
||||
"Hashtag Blocked": "Hashtag bloqué",
|
||||
"This is a blogging instance": "Ceci est une instance de blog",
|
||||
"Edit Links": "Modifier les liens",
|
||||
"One link per line. Description followed by the link.": "Un lien par ligne. Description suivie du lien.",
|
||||
"Left column image": "Image de la colonne de gauche",
|
||||
"Right column image": "Image de la colonne de droite",
|
||||
"RSS feed for this site": "Flux RSS de ce site",
|
||||
"Edit newswire": "Modifier le fil d'actualité",
|
||||
"Add RSS feed links below.": "Ajoutez des liens de flux RSS ci-dessous.",
|
||||
"Newswire RSS Feed": "Flux RSS de Newswire"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,5 +287,14 @@
|
|||
"Autogenerated Hashtags": "Hashtags uathghinte",
|
||||
"Autogenerated Content Warnings": "Rabhaidh Ábhar Uathghinte",
|
||||
"Indymedia": "Indymedia",
|
||||
"Hashtag Blocked": "Hashtag Blocáilte"
|
||||
"Hashtag Blocked": "Hashtag Blocáilte",
|
||||
"This is a blogging instance": "Seo sampla blagála",
|
||||
"Edit Links": "Cuir Naisc in eagar",
|
||||
"One link per line. Description followed by the link.": "Nasc amháin in aghaidh an líne. Cur síos agus an nasc ina dhiaidh sin.",
|
||||
"Left column image": "Íomhá colún ar chlé",
|
||||
"Right column image": "Íomhá colún ar dheis",
|
||||
"RSS feed for this site": "Fotha RSS don láithreán seo",
|
||||
"Edit newswire": "Cuir sreang nuachta in eagar",
|
||||
"Add RSS feed links below.": "Cuir naisc beatha RSS thíos.",
|
||||
"Newswire RSS Feed": "Newswire RSS Feed"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,5 +287,14 @@
|
|||
"Autogenerated Hashtags": "ऑटोजेनरेटेड हैशटैग",
|
||||
"Autogenerated Content Warnings": "स्वतः प्राप्त सामग्री चेतावनी",
|
||||
"Indymedia": "Indymedia",
|
||||
"Hashtag Blocked": "हैशटैग अवरुद्ध"
|
||||
"Hashtag Blocked": "हैशटैग अवरुद्ध",
|
||||
"This is a blogging instance": "यह एक ब्लॉगिंग उदाहरण है",
|
||||
"Edit Links": "लिंक संपादित करें",
|
||||
"One link per line. Description followed by the link.": "प्रति पंक्ति एक लिंक। लिंक के बाद विवरण।",
|
||||
"Left column image": "बाएं स्तंभ की छवि",
|
||||
"Right column image": "राइट कॉलम छवि",
|
||||
"RSS feed for this site": "इस साइट के लिए आरएसएस फ़ीड",
|
||||
"Edit newswire": "नवांश संपादित करें",
|
||||
"Add RSS feed links below.": "नीचे आरएसएस फ़ीड लिंक जोड़ें।",
|
||||
"Newswire RSS Feed": "Newswire RSS फ़ीड"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,5 +287,14 @@
|
|||
"Autogenerated Hashtags": "Hashtag generati automaticamente",
|
||||
"Autogenerated Content Warnings": "Avvisi sui contenuti generati automaticamente",
|
||||
"Indymedia": "Indymedia",
|
||||
"Hashtag Blocked": "Hashtag bloccato"
|
||||
"Hashtag Blocked": "Hashtag bloccato",
|
||||
"This is a blogging instance": "Questa è un'istanza di blog",
|
||||
"Edit Links": "Modifica collegamenti",
|
||||
"One link per line. Description followed by the link.": "Un collegamento per riga. Descrizione seguita dal collegamento.",
|
||||
"Left column image": "Immagine della colonna di sinistra",
|
||||
"Right column image": "Immagine della colonna di destra",
|
||||
"RSS feed for this site": "Feed RSS per questo sito",
|
||||
"Edit newswire": "Modifica newswire",
|
||||
"Add RSS feed links below.": "Aggiungi i link ai feed RSS di seguito.",
|
||||
"Newswire RSS Feed": "Feed RSS di Newswire"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,5 +287,14 @@
|
|||
"Autogenerated Hashtags": "自動生成されたハッシュタグ",
|
||||
"Autogenerated Content Warnings": "自動生成されたコンテンツの警告",
|
||||
"Indymedia": "Indymedia",
|
||||
"Hashtag Blocked": "ハッシュタグがブロックされました"
|
||||
"Hashtag Blocked": "ハッシュタグがブロックされました",
|
||||
"This is a blogging instance": "これはブログのインスタンスです",
|
||||
"Edit Links": "リンクの編集",
|
||||
"One link per line. Description followed by the link.": "1行に1つのリンク。 説明の後にリンクが続きます。",
|
||||
"Left column image": "左の列の画像",
|
||||
"Right column image": "右の列の画像",
|
||||
"RSS feed for this site": "このサイトのRSSフィード",
|
||||
"Edit newswire": "ニュースワイヤーを編集",
|
||||
"Add RSS feed links below.": "以下にRSSフィードリンクを追加します。",
|
||||
"Newswire RSS Feed": "NewswireRSSフィード"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,5 +283,14 @@
|
|||
"Autogenerated Hashtags": "Autogenerated Hashtags",
|
||||
"Autogenerated Content Warnings": "Autogenerated Content Warnings",
|
||||
"Indymedia": "Indymedia",
|
||||
"Hashtag Blocked": "Hashtag Blocked"
|
||||
"Hashtag Blocked": "Hashtag Blocked",
|
||||
"This is a blogging instance": "This is a blogging instance",
|
||||
"Edit Links": "Edit Links",
|
||||
"One link per line. Description followed by the link.": "One link per line. Description followed by the link. Titles should begin with #",
|
||||
"Left column image": "Left column image",
|
||||
"Right column image": "Right column image",
|
||||
"RSS feed for this site": "RSS feed for this site",
|
||||
"Edit newswire": "Edit newswire",
|
||||
"Add RSS feed links below.": "Add RSS feed links below.",
|
||||
"Newswire RSS Feed": "Newswire RSS Feed"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,5 +287,14 @@
|
|||
"Autogenerated Hashtags": "Hashtags autogeradas",
|
||||
"Autogenerated Content Warnings": "Avisos de conteúdo gerado automaticamente",
|
||||
"Indymedia": "Indymedia",
|
||||
"Hashtag Blocked": "Hashtag bloqueada"
|
||||
"Hashtag Blocked": "Hashtag bloqueada",
|
||||
"This is a blogging instance": "Esta é uma instância de blog",
|
||||
"Edit Links": "Editar Links",
|
||||
"One link per line. Description followed by the link.": "Um link por linha. Descrição seguida pelo link.",
|
||||
"Left column image": "Imagem da coluna esquerda",
|
||||
"Right column image": "Imagem da coluna direita",
|
||||
"RSS feed for this site": "Feed RSS para este site",
|
||||
"Edit newswire": "Editar notícias",
|
||||
"Add RSS feed links below.": "Adicione links de feed RSS abaixo.",
|
||||
"Newswire RSS Feed": "Feed RSS da Newswire"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,5 +287,14 @@
|
|||
"Autogenerated Hashtags": "Автоматически сгенерированные хештеги",
|
||||
"Autogenerated Content Warnings": "Автоматические предупреждения о содержании",
|
||||
"Indymedia": "Indymedia",
|
||||
"Hashtag Blocked": "Хештег заблокирован"
|
||||
"Hashtag Blocked": "Хештег заблокирован",
|
||||
"This is a blogging instance": "Это экземпляр блога",
|
||||
"Edit Links": "Редактировать ссылки",
|
||||
"One link per line. Description followed by the link.": "По одной ссылке в строке. Описание с последующей ссылкой.",
|
||||
"Left column image": "Изображение в левом столбце",
|
||||
"Right column image": "Изображение в правом столбце",
|
||||
"RSS feed for this site": "RSS-канал для этого сайта",
|
||||
"Edit newswire": "Редактировать ленту новостей",
|
||||
"Add RSS feed links below.": "Добавьте ссылки на RSS-канал ниже.",
|
||||
"Newswire RSS Feed": "Лента новостей RSS"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,5 +287,14 @@
|
|||
"Autogenerated Hashtags": "自动生成的标签",
|
||||
"Autogenerated Content Warnings": "自动生成的内容警告",
|
||||
"Indymedia": "Indymedia",
|
||||
"Hashtag Blocked": "标签被阻止"
|
||||
"Hashtag Blocked": "标签被阻止",
|
||||
"This is a blogging instance": "这是一个博客实例",
|
||||
"Edit Links": "编辑连结",
|
||||
"One link per line. Description followed by the link.": "每行一个链接。 描述,然后是链接。",
|
||||
"Left column image": "左栏图片",
|
||||
"Right column image": "右栏图片",
|
||||
"RSS feed for this site": "该站点的RSS feed",
|
||||
"Edit newswire": "编辑新闻专线",
|
||||
"Add RSS feed links below.": "在下面添加RSS feed链接。",
|
||||
"Newswire RSS Feed": "Newswire RSS提要"
|
||||
}
|
||||
|
|
|
|||