Editing news posts

main
Bob Mottram 2020-10-10 20:14:36 +01:00
parent fa945479b5
commit 5085587ad4
19 changed files with 325 additions and 18 deletions

174
daemon.py
View File

@ -62,6 +62,7 @@ from person import canRemovePost
from person import personSnooze
from person import personUnsnooze
from posts import isModerator
from posts import isEditor
from posts import mutePost
from posts import unmutePost
from posts import createQuestionPost
@ -146,6 +147,7 @@ from webinterface import htmlProfileAfterSearch
from webinterface import htmlEditProfile
from webinterface import htmlEditLinks
from webinterface import htmlEditNewswire
from webinterface import htmlEditNewsPost
from webinterface import htmlTermsOfService
from webinterface import htmlSkillsSearch
from webinterface import htmlHistorySearch
@ -1096,7 +1098,7 @@ class PubServer(BaseHTTPRequestHandler):
return True
elif '/' + nickname + '?' in self.path:
return True
elif self.path.endswith('/'+nickname):
elif self.path.endswith('/' + nickname):
return True
print('AUTH: nickname ' + nickname +
' was not found in path ' + self.path)
@ -2929,6 +2931,133 @@ class PubServer(BaseHTTPRequestHandler):
cookie, callingDomain)
self.server.POSTbusy = False
def _newsPostEdit(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):
"""edits a news post
"""
usersPath = path.replace('/newseditdata', '')
usersPath = usersPath.replace('/editnewspost', '')
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)
editorRole = None
if nickname:
editorRole = isEditor(baseDir, nickname)
if not nickname or not editorRole:
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 an editor' + 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 news 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
# extract all of the text fields into a dict
fields = \
extractTextFieldsInPOST(postBytes, boundary, debug)
newsPostUrl = None
newsPostTitle = None
newsPostContent = None
if fields.get('newsPostUrl'):
newsPostUrl = fields['newsPostUrl']
if fields.get('newsPostTitle'):
newsPostTitle = fields['newsPostTitle']
if fields.get('editedNewsPost'):
newsPostContent = fields['editedNewsPost']
if newsPostUrl and newsPostContent and newsPostTitle:
# load the post
postFilename = \
locatePost(baseDir, nickname, domain,
newsPostUrl)
if postFilename:
postJsonObject = loadJson(postFilename)
# update the content and title
postJsonObject['object']['summary'] = \
newsPostTitle
postJsonObject['object']['content'] = \
newsPostContent
postUrl = postJsonObject['object']['id']
postUrl = postUrl.replace('/', '#')
# remove the html from post cache
cachedPost = \
baseDir + '/accounts/' + \
nickname + '@' + domain + \
'/postcache/' + postUrl + '.html'
if os.path.isfile(cachedPost):
os.remove(cachedPost)
# save the news post
saveJson(postJsonObject, postFilename)
# 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,
@ -7976,6 +8105,29 @@ class PubServer(BaseHTTPRequestHandler):
return True
return False
def _editNewsPost(self, callingDomain: str, path: str,
translate: {}, baseDir: str,
httpPrefix: str, domain: str, port: int,
cookie: str) -> bool:
"""Show the edit screen for a news post
"""
if '/users/' in path and '/editnewspost=' in path:
postUrl = path.split('/editnewspost=')[1]
if '?' in postUrl:
postUrl = postUrl.split('?')[0]
msg = htmlEditNewsPost(translate, baseDir,
path, domain, port,
httpPrefix, postUrl).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: {},
@ -9357,6 +9509,16 @@ class PubServer(BaseHTTPRequestHandler):
cookie):
return
# edit news post
if self._editNewsPost(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,
@ -10845,6 +11007,16 @@ class PubServer(BaseHTTPRequestHandler):
self.server.defaultTimeline)
return
if authorized and self.path.endswith('/newseditdata'):
self._newsPostEdit(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

View File

@ -35,7 +35,6 @@ from auth import removePassword
from roles import setRole
from media import removeMetaData
from utils import validNickname
from utils import noOfAccounts
from utils import loadJson
from utils import saveJson
from utils import setConfigParam
@ -450,6 +449,7 @@ def createPerson(baseDir: str, nickname: str, domain: str, port: int,
setConfigParam(baseDir, 'admin', nickname)
setRole(baseDir, nickname, domain, 'instance', 'admin')
setRole(baseDir, nickname, domain, 'instance', 'moderator')
setRole(baseDir, nickname, domain, 'instance', 'editor')
setRole(baseDir, nickname, domain, 'instance', 'delegator')
if not os.path.isdir(baseDir + '/accounts'):

View File

@ -96,6 +96,34 @@ def isModerator(baseDir: str, nickname: str) -> bool:
return False
def isEditor(baseDir: str, nickname: str) -> bool:
"""Returns true if the given nickname is an editor
"""
editorsFile = baseDir + '/accounts/editors.txt'
if not os.path.isfile(editorsFile):
adminName = getConfigParam(baseDir, 'admin')
if not adminName:
return False
if adminName == nickname:
return True
return False
with open(editorsFile, "r") as f:
lines = f.readlines()
if len(lines) == 0:
adminName = getConfigParam(baseDir, 'admin')
if not adminName:
return False
if adminName == nickname:
return True
for editor in lines:
editor = editor.strip('\n').strip('\r')
if editor == nickname:
return True
return False
def noOfFollowersOnDomain(baseDir: str, handle: str,
domain: str, followFile='followers.txt') -> int:
"""Returns the number of followers of the given handle from the given domain

View File

@ -305,5 +305,6 @@
"Remove Vote": "إزالة التصويت",
"This is a news instance": "هذا مثال أخبار",
"News": "أخبار",
"Read more...": "اقرأ أكثر..."
"Read more...": "اقرأ أكثر...",
"Edit News Post": ""
}

View File

@ -305,5 +305,6 @@
"Remove Vote": "Elimina el vot",
"This is a news instance": "Aquesta és una instància de notícies",
"News": "Notícies",
"Read more...": "Llegeix més..."
"Read more...": "Llegeix més...",
"Edit News Post": ""
}

View File

@ -305,5 +305,6 @@
"Remove Vote": "Tynnwch y Bleidlais",
"This is a news instance": "Dyma enghraifft newyddion",
"News": "Newyddion",
"Read more...": "Darllen mwy..."
"Read more...": "Darllen mwy...",
"Edit News Post": ""
}

View File

@ -305,5 +305,6 @@
"Remove Vote": "Abstimmung entfernen",
"This is a news instance": "Dies ist eine Nachrichteninstanz",
"News": "Nachrichten",
"Read more...": "Weiterlesen..."
"Read more...": "Weiterlesen...",
"Edit News Post": ""
}

View File

@ -305,5 +305,6 @@
"Remove Vote": "Remove Vote",
"This is a news instance": "This is a news instance",
"News": "News",
"Read more...": "Read more..."
"Read more...": "Read more...",
"Edit News Post": "Edit News Post"
}

View File

@ -305,5 +305,6 @@
"Remove Vote": "Eliminar voto",
"This is a news instance": "Esta es una instancia de noticias",
"News": "Noticias",
"Read more...": "Lee mas..."
"Read more...": "Lee mas...",
"Edit News Post": ""
}

View File

@ -305,5 +305,6 @@
"Remove Vote": "Supprimer le vote",
"This is a news instance": "Ceci est une instance d'actualité",
"News": "Nouvelles",
"Read more...": "Lire la suite..."
"Read more...": "Lire la suite...",
"Edit News Post": ""
}

View File

@ -305,5 +305,6 @@
"Remove Vote": "Bain Vóta",
"This is a news instance": "Is sampla nuachta é seo",
"News": "Nuacht",
"Read more...": "Leigh Nios mo..."
"Read more...": "Leigh Nios mo...",
"Edit News Post": ""
}

View File

@ -305,5 +305,6 @@
"Remove Vote": "वोट हटा दें",
"This is a news instance": "यह एक समाचार का उदाहरण है",
"News": "समाचार",
"Read more...": "अधिक पढ़ें..."
"Read more...": "अधिक पढ़ें...",
"Edit News Post": ""
}

View File

@ -305,5 +305,6 @@
"Remove Vote": "Rimuovi voto",
"This is a news instance": "Questa è un'istanza di notizie",
"News": "Notizia",
"Read more...": "Leggi di più..."
"Read more...": "Leggi di più...",
"Edit News Post": ""
}

View File

@ -305,5 +305,6 @@
"Remove Vote": "投票を削除",
"This is a news instance": "これはニュースインスタンスです",
"News": "ニュース",
"Read more...": "続きを読む..."
"Read more...": "続きを読む...",
"Edit News Post": ""
}

View File

@ -301,5 +301,6 @@
"Remove Vote": "Remove Vote",
"This is a news instance": "This is a news instance",
"News": "News",
"Read more...": "Read more..."
"Read more...": "Read more...",
"Edit News Post": "Edit News Post"
}

View File

@ -305,5 +305,6 @@
"Remove Vote": "Remover voto",
"This is a news instance": "Esta é uma instância de notícias",
"News": "Notícia",
"Read more...": "Consulte Mais informação..."
"Read more...": "Consulte Mais informação...",
"Edit News Post": ""
}

View File

@ -305,5 +305,6 @@
"Remove Vote": "Удалить голос",
"This is a news instance": "Это новостной экземпляр",
"News": "Новости",
"Read more...": "Подробнее..."
"Read more...": "Подробнее...",
"Edit News Post": ""
}

View File

@ -305,5 +305,6 @@
"Remove Vote": "删除投票",
"This is a news instance": "这是一个新闻实例",
"News": "新闻",
"Read more...": "阅读更多..."
"Read more...": "阅读更多...",
"Edit News Post": ""
}

View File

@ -52,6 +52,7 @@ from posts import getUserUrl
from posts import parseUserFeed
from posts import populateRepliesJson
from posts import isModerator
from posts import isEditor
from posts import downloadAnnounce
from session import getJson
from auth import createPassword
@ -1346,6 +1347,85 @@ def htmlEditNewswire(translate: {}, baseDir: str, path: str,
return editNewswireForm
def htmlEditNewsPost(translate: {}, baseDir: str, path: str,
domain: str, port: int,
httpPrefix: str, postUrl: str) -> str:
"""Edits a news post
"""
if '/users/' not in path:
return ''
pathOriginal = path
path = path.replace('/inbox', '').replace('/outbox', '')
path = path.replace('/shares', '')
path = path.replace('/tlnews', '')
nickname = getNicknameFromActor(path)
if not nickname:
return ''
# is the user an editor?
if not isEditor(baseDir, nickname):
return ''
postFilename = locatePost(baseDir, nickname, domain, postUrl)
if not postFilename:
return ''
postJsonObject = loadJson(postFilename)
if not postJsonObject:
return ''
cssFilename = baseDir + '/epicyon-links.css'
if os.path.isfile(baseDir + '/links.css'):
cssFilename = baseDir + '/links.css'
with open(cssFilename, 'r') as cssFile:
editCSS = cssFile.read()
if httpPrefix != 'https':
editCSS = \
editCSS.replace('https://', httpPrefix + '://')
editNewsPostForm = htmlHeader(cssFilename, editCSS)
editNewsPostForm += \
'<form enctype="multipart/form-data" method="POST" ' + \
'accept-charset="UTF-8" action="' + path + '/newseditdata">\n'
editNewsPostForm += \
' <div class="vertical-center">\n'
editNewsPostForm += \
' <p class="new-post-text">' + translate['Edit News Post'] + '</p>'
editNewsPostForm += \
' <div class="container">\n'
editNewsPostForm += \
' <a href="' + pathOriginal + '"><button class="cancelbtn">' + \
translate['Go Back'] + '</button></a>\n'
editNewsPostForm += \
' <input type="submit" name="submitEditedNewsPost" value="' + \
translate['Submit'] + '">\n'
editNewsPostForm += \
' </div>\n'
editNewsPostForm += \
'<div class="container">'
editNewsPostForm += \
' <input type="hidden" name="newsPostUrl" value="' + \
postUrl + '">\n'
newsPostTitle = postJsonObject['object']['summary']
editNewsPostForm += \
' <input type="text" name="newsPostTitle" value="' + \
newsPostTitle + '"><br>\n'
newsPostContent = postJsonObject['object']['content']
editNewsPostForm += \
' <textarea id="message" name="editedNewsPost" ' + \
'style="height:600px">' + newsPostContent + '</textarea>'
editNewsPostForm += \
'</div>'
editNewsPostForm += htmlFooter()
return editNewsPostForm
def htmlEditProfile(translate: {}, baseDir: str, path: str,
domain: str, port: int, httpPrefix: str) -> str:
"""Shows the edit profile screen
@ -4515,8 +4595,8 @@ def individualPostAsHtml(allowDownloads: bool,
if fullDomain + '/users/' + nickname in postJsonObject['actor']:
if '/statuses/' in postJsonObject['object']['id']:
if isBlogPost(postJsonObject):
blogPostId = postJsonObject['object']['id']
if not isNewsPost(postJsonObject):
blogPostId = postJsonObject['object']['id']
editStr += \
' ' + \
'<a class="imageAnchor" href="/users/' + \
@ -4529,6 +4609,18 @@ def individualPostAsHtml(allowDownloads: bool,
translate['Edit blog post'] + '" alt="' + \
translate['Edit blog post'] + \
' |" src="/' + iconsDir + '/edit.png"/></a>\n'
else:
editStr += \
' ' + \
'<a class="imageAnchor" href="/users/' + \
nickname + '/editnewspost=' + \
blogPostId.replace('/', '#') + \
'?actor=' + actorNickname + \
'" title="' + translate['Edit blog post'] + '">' + \
'<img loading="lazy" title="' + \
translate['Edit blog post'] + '" alt="' + \
translate['Edit blog post'] + \
' |" src="/' + iconsDir + '/edit.png"/></a>\n'
elif isEvent:
eventPostId = postJsonObject['object']['id']
editStr += \