diff --git a/daemon.py b/daemon.py index 4217f30d..71687f4c 100644 --- a/daemon.py +++ b/daemon.py @@ -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 diff --git a/person.py b/person.py index 26b88316..f4539e18 100644 --- a/person.py +++ b/person.py @@ -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'): diff --git a/posts.py b/posts.py index 37215b62..441802ac 100644 --- a/posts.py +++ b/posts.py @@ -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 diff --git a/translations/ar.json b/translations/ar.json index 8250b98c..4e66db47 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -305,5 +305,6 @@ "Remove Vote": "إزالة التصويت", "This is a news instance": "هذا مثال أخبار", "News": "أخبار", - "Read more...": "اقرأ أكثر..." + "Read more...": "اقرأ أكثر...", + "Edit News Post": "" } diff --git a/translations/ca.json b/translations/ca.json index c8c0e18a..b1f7fedf 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -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": "" } diff --git a/translations/cy.json b/translations/cy.json index 98dc73a7..11d10979 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -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": "" } diff --git a/translations/de.json b/translations/de.json index b53008f7..2ba6a4c3 100644 --- a/translations/de.json +++ b/translations/de.json @@ -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": "" } diff --git a/translations/en.json b/translations/en.json index 7aa26674..7f101427 100644 --- a/translations/en.json +++ b/translations/en.json @@ -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" } diff --git a/translations/es.json b/translations/es.json index e5538332..d6ebcf97 100644 --- a/translations/es.json +++ b/translations/es.json @@ -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": "" } diff --git a/translations/fr.json b/translations/fr.json index 87851e3d..d7c8075a 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -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": "" } diff --git a/translations/ga.json b/translations/ga.json index 8e9ece95..dcefac7f 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -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": "" } diff --git a/translations/hi.json b/translations/hi.json index 53af28fb..b9141263 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -305,5 +305,6 @@ "Remove Vote": "वोट हटा दें", "This is a news instance": "यह एक समाचार का उदाहरण है", "News": "समाचार", - "Read more...": "अधिक पढ़ें..." + "Read more...": "अधिक पढ़ें...", + "Edit News Post": "" } diff --git a/translations/it.json b/translations/it.json index 6fbda658..26798c74 100644 --- a/translations/it.json +++ b/translations/it.json @@ -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": "" } diff --git a/translations/ja.json b/translations/ja.json index f68c8fcf..cb63580e 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -305,5 +305,6 @@ "Remove Vote": "投票を削除", "This is a news instance": "これはニュースインスタンスです", "News": "ニュース", - "Read more...": "続きを読む..." + "Read more...": "続きを読む...", + "Edit News Post": "" } diff --git a/translations/oc.json b/translations/oc.json index 55d94dda..8fad854d 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -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" } diff --git a/translations/pt.json b/translations/pt.json index 3562ad31..e11ff1b0 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -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": "" } diff --git a/translations/ru.json b/translations/ru.json index 871742e1..65fdbb3e 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -305,5 +305,6 @@ "Remove Vote": "Удалить голос", "This is a news instance": "Это новостной экземпляр", "News": "Новости", - "Read more...": "Подробнее..." + "Read more...": "Подробнее...", + "Edit News Post": "" } diff --git a/translations/zh.json b/translations/zh.json index 13844c25..55ca8768 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -305,5 +305,6 @@ "Remove Vote": "删除投票", "This is a news instance": "这是一个新闻实例", "News": "新闻", - "Read more...": "阅读更多..." + "Read more...": "阅读更多...", + "Edit News Post": "" } diff --git a/webinterface.py b/webinterface.py index def0b025..62415312 100644 --- a/webinterface.py +++ b/webinterface.py @@ -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 += \ + '
\n' + editNewsPostForm += \ + '
\n' + editNewsPostForm += \ + '

' + translate['Edit News Post'] + '

' + editNewsPostForm += \ + '
\n' + editNewsPostForm += \ + ' \n' + editNewsPostForm += \ + ' \n' + editNewsPostForm += \ + '
\n' + + editNewsPostForm += \ + '
' + + editNewsPostForm += \ + ' \n' + + newsPostTitle = postJsonObject['object']['summary'] + editNewsPostForm += \ + '
\n' + + newsPostContent = postJsonObject['object']['content'] + editNewsPostForm += \ + ' ' + + editNewsPostForm += \ + '
' + + 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 += \ ' ' + \ '\n' + else: + editStr += \ + ' ' + \ + '' + \ + '' + \
+                        translate['Edit blog post'] + \
+                        ' |\n' elif isEvent: eventPostId = postJsonObject['object']['id'] editStr += \