diff --git a/daemon.py b/daemon.py index 8db2d2d91..327d2c127 100644 --- a/daemon.py +++ b/daemon.py @@ -67,6 +67,7 @@ from webinterface import htmlIndividualPost from webinterface import htmlProfile from webinterface import htmlInbox from webinterface import htmlOutbox +from webinterface import htmlModeration from webinterface import htmlPostReplies from webinterface import htmlLogin from webinterface import htmlGetLoginCredentials @@ -1185,8 +1186,9 @@ class PubServer(BaseHTTPRequestHandler): self._404() self.server.GETbusy=False return + # get the inbox for a given person - if self.path.endswith('/inbox'): + if self.path.endswith('/inbox') or '/inbox?page=' in self.path: if '/users/' in self.path: if authorized: inboxFeed=personBoxJson(self.server.baseDir, \ @@ -1243,7 +1245,7 @@ class PubServer(BaseHTTPRequestHandler): print('DEBUG: GET access to inbox is unauthorized') self.send_response(405) self.end_headers() - self.server.POSTbusy=False + self.server.GETbusy=False return # get outbox feed for a person @@ -1290,6 +1292,67 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy=False return + # get the moderation feed for a moderator + if self.path.endswith('/moderation') or '/moderation?page=' in self.path: + if '/users/' in self.path: + if authorized: + moderationFeed= \ + personBoxJson(self.server.baseDir, \ + self.server.domain, \ + self.server.port, \ + self.path, \ + self.server.httpPrefix, \ + maxPostsInFeed, 'moderation', \ + True,self.server.ocapAlways) + if moderationFeed: + if 'text/html' in self.headers['Accept']: + nickname=self.path.replace('/users/','').replace('/moderation','') + pageNumber=1 + if '?page=' in nickname: + pageNumber=nickname.split('?page=')[1] + nickname=nickname.split('?page=')[0] + if pageNumber.isdigit(): + pageNumber=int(pageNumber) + else: + pageNumber=1 + if 'page=' not in self.path: + # if no page was specified then show the first + moderationFeed= \ + personBoxJson(self.server.baseDir, \ + self.server.domain, \ + self.server.port, \ + self.path+'?page=1', \ + self.server.httpPrefix, \ + maxPostsInFeed, 'moderation', \ + True,self.server.ocapAlways) + self._set_headers('text/html',cookie) + self.wfile.write(htmlModeration(pageNumber,maxPostsInFeed, \ + self.server.session, \ + self.server.baseDir, \ + self.server.cachedWebfingers, \ + self.server.personCache, \ + nickname, \ + self.server.domain, \ + self.server.port, \ + moderationFeed, \ + True).encode('utf-8')) + else: + self._set_headers('application/json',None) + self.wfile.write(json.dumps(moderationFeed).encode('utf-8')) + self.server.GETbusy=False + return + else: + if self.server.debug: + nickname=self.path.replace('/users/','').replace('/moderation','') + print('DEBUG: '+nickname+ \ + ' was not authorized to access '+self.path) + if self.server.debug: + print('DEBUG: GET access to moderation feed is unauthorized') + self.send_response(405) + self.end_headers() + self.server.GETbusy=False + return + shares=getSharesFeedForPerson(self.server.baseDir, \ self.server.domain, \ self.server.port,self.path, \ diff --git a/epicyon-profile.css b/epicyon-profile.css index 390b7e5b3..3000222c3 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -151,7 +151,7 @@ a:link { text-align: center; font-size: 18px; padding: 10px; - width: 20%; + width: 15%; max-width: 200px; min-width: 100px; transition: all 0.5s; diff --git a/person.py b/person.py index 767a5bfac..4a6912050 100644 --- a/person.py +++ b/person.py @@ -19,6 +19,7 @@ from webfinger import createWebfingerEndpoint from webfinger import storeWebfingerEndpoint from posts import createInbox from posts import createOutbox +from posts import createModeration from auth import storeBasicCredentials from roles import setRole from media import removeMetaData @@ -340,9 +341,9 @@ def personLookup(domain: str,path: str,baseDir: str) -> {}: def personBoxJson(baseDir: str,domain: str,port: int,path: str, \ httpPrefix: str,noOfItems: int,boxname: str, \ authorized: bool,ocapAlways: bool) -> []: - """Obtain the inbox/outbox feed for the given person + """Obtain the inbox/outbox/moderation feed for the given person """ - if boxname!='inbox' and boxname!='outbox': + if boxname!='inbox' and boxname!='outbox' and boxname!='moderation': return None if not '/'+boxname in path: @@ -379,8 +380,13 @@ def personBoxJson(baseDir: str,domain: str,port: int,path: str, \ if boxname=='inbox': return createInbox(baseDir,nickname,domain,port,httpPrefix, \ noOfItems,headerOnly,ocapAlways,pageNumber) - return createOutbox(baseDir,nickname,domain,port,httpPrefix, \ - noOfItems,headerOnly,authorized,pageNumber) + elif boxname=='outbox': + return createOutbox(baseDir,nickname,domain,port,httpPrefix, \ + noOfItems,headerOnly,authorized,pageNumber) + elif boxname=='moderation': + return createModeration(baseDir,nickname,domain,port,httpPrefix, \ + noOfItems,headerOnly,authorized,pageNumber) + return None def personInboxJson(baseDir: str,domain: str,port: int,path: str, \ httpPrefix: str,noOfItems: int,ocapAlways: bool) -> []: diff --git a/posts.py b/posts.py index 3d075788f..123009b04 100644 --- a/posts.py +++ b/posts.py @@ -47,6 +47,27 @@ try: except ImportError: from bs4 import BeautifulSoup +def isModerator(baseDir: str,nickname: str) -> bool: + """Returns true if the given nickname is a moderator + """ + moderatorsFile=baseDir+'/accounts/moderators.txt' + + if not os.path.isfile(moderatorsFile): + if getConfigParam(baseDir,'admin')==nickname: + return True + return False + + with open(moderatorsFile, "r") as f: + lines = f.readlines() + if len(lines)==0: + if getConfigParam(baseDir,'admin')==nickname: + return True + for moderator in lines: + moderator=moderator.strip('\n') + if moderator==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 @@ -551,10 +572,17 @@ def createPostBase(baseDir: str,nickname: str, domain: str, port: int, \ # if this is a moderation report then add a status if isModerationReport: + # add status if newPost.get('object'): newPost['object']['moderationStatus']='pending' else: newPost['moderationStatus']='pending' + # save to index file + moderationIndexFile=baseDir+'/accounts/moderation.txt' + modFile=open(moderationIndexFile, "a+") + if modFile: + modFile.write(newPostId+'\n') + modFile.close() if saveToFile: savePostToBox(baseDir,httpPrefix,newPostId, \ @@ -1298,11 +1326,69 @@ def createInbox(baseDir: str,nickname: str,domain: str,port: int,httpPrefix: str itemsPerPage: int,headerOnly: bool,ocapAlways: bool,pageNumber=None) -> {}: return createBoxBase(baseDir,'inbox',nickname,domain,port,httpPrefix, \ itemsPerPage,headerOnly,True,ocapAlways,pageNumber) + def createOutbox(baseDir: str,nickname: str,domain: str,port: int,httpPrefix: str, \ itemsPerPage: int,headerOnly: bool,authorized: bool,pageNumber=None) -> {}: return createBoxBase(baseDir,'outbox',nickname,domain,port,httpPrefix, \ itemsPerPage,headerOnly,authorized,False,pageNumber) +def createModeration(baseDir: str,nickname: str,domain: str,port: int,httpPrefix: str, \ + itemsPerPage: int,headerOnly: bool,ocapAlways: bool,pageNumber=None) -> {}: + boxDir = createPersonDir(nickname,domain,baseDir,'inbox') + boxname='moderation' + + if port!=80 and port!=443: + domain = domain+':'+str(port) + + if not pageNumber: + pageNumber=1 + + pageStr='?page='+str(pageNumber) + boxHeader = {'@context': 'https://www.w3.org/ns/activitystreams', + 'first': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname+'?page=true', + 'id': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname, + 'last': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname+'?page=true', + 'totalItems': 0, + 'type': 'OrderedCollection'} + boxItems = {'@context': 'https://www.w3.org/ns/activitystreams', + 'id': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname+pageStr, + 'orderedItems': [ + ], + 'partOf': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname, + 'type': 'OrderedCollectionPage'} + + if isModerator(baseDir,nickname): + moderationIndexFile=baseDir+'/accounts/moderation.txt' + if os.path.isfile(moderationIndexFile): + with open(moderationIndexFile, "r") as f: + lines = f.readlines() + boxHeader['totalItems']=len(lines) + if headerOnly: + return boxHeader + + pageLines=[] + if len(lines)>0: + endLineNumber=len(lines)-1-int(itemsPerPage*pageNumber) + if endLineNumber<0: + endLineNumber=0 + startLineNumber=len(lines)-1-int(itemsPerPage*(pageNumber-1)) + if startLineNumber<0: + startLineNumber=0 + lineNumber=startLineNumber + while lineNumber>=endLineNumber: + pageLines.append(lines[lineNumber].strip('\n')) + lineNumber-=1 + + for postUrl in pageLines: + postFilename=boxDir+'/'+postUrl.replace('/','#')+'.json' + if os.path.isfile(postFilename): + with open(postFilename, 'r') as fp: + postJsonObject=commentjson.load(fp) + boxItems['orderedItems'].append(postJsonObject) + if headerOnly: + return boxHeader + return boxItems + def getStatusNumberFromPostFilename(filename) -> int: """Gets the status number from a post filename eg. https:##testdomain.com:8085#users#testuser567#statuses#1562958506952068.json diff --git a/utils.py b/utils.py index 3cea32ebb..922330c84 100644 --- a/utils.py +++ b/utils.py @@ -175,6 +175,23 @@ def deletePost(baseDir: str,httpPrefix: str,nickname: str,domain: str,postFilena # remove any attachment removeAttachment(baseDir,httpPrefix,domain,postJsonObject) + + # remove from moderation index file + if postJsonObject.get('moderationStatus'): + moderationIndexFile=baseDir+'/accounts/moderation.txt' + if os.path.isfile(moderationIndexFile): + if postJsonObject.get('object'): + if isinstance(postJsonObject['object'], dict): + if postJsonObject['object'].get('id'): + # get the id of the post + postId=postJsonObject['object']['id'].replace('/activity','') + if postId in open(moderationIndexFile).read(): + with open(moderationIndexFile, "r") as f: + lines = f.readlines() + with open(moderationIndexFile, "w+") as f: + for line in lines: + if line.strip("\n") != postId: + f.write(line) # remove any hashtags index entries removeHashtagIndex=False diff --git a/webinterface.py b/webinterface.py index 970c2d0bc..e972c7293 100644 --- a/webinterface.py +++ b/webinterface.py @@ -25,6 +25,7 @@ from posts import getPersonBox from posts import getUserUrl from posts import parseUserFeed from posts import populateRepliesJson +from posts import isModerator from session import getJson from auth import createPassword from like import likedByPerson @@ -34,6 +35,17 @@ from content import getMentionsFromHtml from config import getConfigParam from skills import getSkills +def noOfModerationPosts(baseDir: str) -> int: + """Returns the number of posts addressed to moderators + """ + moderationIndexFile=baseDir+'/accounts/moderation.txt' + if not os.path.isfile(moderationIndexFile): + return 0 + with open(moderationIndexFile, "r") as f: + lines = f.readlines() + return len(lines) + return 0 + def htmlHashtagSearch(baseDir: str,hashtag: str,pageNumber: int,postsPerPage: int, session,wfRequest: {},personCache: {}) -> str: """Show a page containing search results for a hashtag @@ -961,12 +973,17 @@ def htmlTimeline(pageNumber: int,itemsPerPage: int,session,baseDir: str, \ cssFile.read().replace('banner.png', \ '/users/'+nickname+'/banner.png') + moderator=isModerator(baseDir,nickname) + inboxButton='button' sentButton='button' + moderationButton='button' if boxName=='inbox': inboxButton='buttonselected' elif boxName=='outbox': sentButton='buttonselected' + elif boxName=='moderation': + moderationButton='buttonselected' actor='/users/'+nickname showIndividualPostIcons=True @@ -983,6 +1000,11 @@ def htmlTimeline(pageNumber: int,itemsPerPage: int,session,baseDir: str, \ followApprovals='Approve follow requests' break + moderationButtonStr='' + if moderator: + if noOfModerationPosts(baseDir)>0: + moderationButtonStr='' + tlStr=htmlHeader(profileStyle) tlStr+= \ '' \ @@ -990,7 +1012,8 @@ def htmlTimeline(pageNumber: int,itemsPerPage: int,session,baseDir: str, \ '' \ '
\n'+ \ ' ' \ - ' ' \ + ' '+ \ + moderationButtonStr+ \ ' Create a new post'+ \ ' Search and follow'+ \ followApprovals+ \ @@ -1019,6 +1042,15 @@ def htmlInbox(pageNumber: int,itemsPerPage: int, \ return htmlTimeline(pageNumber,itemsPerPage,session,baseDir,wfRequest,personCache, \ nickname,domain,port,inboxJson,'inbox',allowDeletion) +def htmlModeration(pageNumber: int,itemsPerPage: int, \ + session,baseDir: str,wfRequest: {},personCache: {}, \ + nickname: str,domain: str,port: int,inboxJson: {}, \ + allowDeletion: bool) -> str: + """Show the moderation feed as html + """ + return htmlTimeline(pageNumber,itemsPerPage,session,baseDir,wfRequest,personCache, \ + nickname,domain,port,inboxJson,'moderation',allowDeletion) + def htmlOutbox(pageNumber: int,itemsPerPage: int, \ session,baseDir: str,wfRequest: {},personCache: {}, \ nickname: str,domain: str,port: int,outboxJson: {}, \