diff --git a/daemon.py b/daemon.py index a6305de5..58b3c389 100644 --- a/daemon.py +++ b/daemon.py @@ -32,6 +32,7 @@ from posts import sendToFollowers from posts import postIsAddressedToPublic from posts import sendToNamedAddresses from posts import createPublicPost +from posts import createReportPost from posts import createUnlistedPost from posts import createFollowersOnlyPost from posts import createDirectMessagePost @@ -948,6 +949,7 @@ class PubServer(BaseHTTPRequestHandler): self.path.endswith('/newunlisted') or \ self.path.endswith('/newfollowers') or \ self.path.endswith('/newdm') or \ + self.path.endswith('/newreport') or \ self.path.endswith('/newshare')): self._set_headers('text/html',cookie) self.wfile.write(htmlNewPost(self.server.baseDir,self.path,inReplyToUrl,replyToList).encode()) @@ -1616,14 +1618,16 @@ class PubServer(BaseHTTPRequestHandler): return -1 if postType=='newdm': - messageJson= \ - createDirectMessagePost(self.server.baseDir, \ - nickname, \ - self.server.domain,self.server.port, \ - self.server.httpPrefix, \ - fields['message'],True,False,False, \ - filename,fields['imageDescription'],True, \ - fields['replyTo'],fields['replyTo'],fields['subject']) + messageJson=None + if '@' in fields['message']: + messageJson= \ + createDirectMessagePost(self.server.baseDir, \ + nickname, \ + self.server.domain,self.server.port, \ + self.server.httpPrefix, \ + fields['message'],True,False,False, \ + filename,fields['imageDescription'],True, \ + fields['replyTo'],fields['replyTo'],fields['subject']) if messageJson: self.postToNickname=nickname if self._postToOutbox(messageJson): @@ -1637,6 +1641,26 @@ class PubServer(BaseHTTPRequestHandler): else: return -1 + if postType=='newreport': + # So as to be sure that this only goes to moderators + # and not accounts being reported we disable any + # included fediverse addresses by replacing '@' with '-at-' + fields['message']=fields['message'].replace('@','-at-') + messageJson= \ + createReportPost(self.server.baseDir, \ + nickname, \ + self.server.domain,self.server.port, \ + self.server.httpPrefix, \ + fields['message'],True,False,False, \ + filename,fields['imageDescription'],True, \ + self.server.debug,fields['subject']) + if messageJson: + self.postToNickname=nickname + if self._postToOutbox(messageJson): + return 1 + else: + return -1 + if postType=='newshare': if not fields.get('itemType'): return False @@ -2164,6 +2188,13 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy=False return postState=self._receiveNewPost(authorized,'newdm') + if postState!=0: + if '/' in nickname: + nickname=nickname.split('/')[0] + self._redirect_headers('/users/'+self.postToNickname+'/outbox',cookie) + self.server.POSTbusy=False + return + postState=self._receiveNewPost(authorized,'newreport') if postState!=0: if '/' in nickname: nickname=nickname.split('/')[0] diff --git a/epicyon-profile.css b/epicyon-profile.css index d77d2de2..8d55a4c5 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -92,6 +92,11 @@ a:link { padding: 4px 0; } +.new-post-subtext { + font-size: 18px; + padding: 4px 0; +} + .highlight { width: 2%; } diff --git a/img/icons/scope_report.png b/img/icons/scope_report.png new file mode 100644 index 00000000..a7802ce9 Binary files /dev/null and b/img/icons/scope_report.png differ diff --git a/posts.py b/posts.py index e6a55a60..7532bd23 100644 --- a/posts.py +++ b/posts.py @@ -41,6 +41,7 @@ from capabilities import capabilitiesUpdate from media import attachImage from content import addHtmlTags from auth import createBasicAuthHeader +from config import getConfigParam try: from BeautifulSoup import BeautifulSoup except ImportError: @@ -748,6 +749,66 @@ def createDirectMessagePost(baseDir: str, attachImageFilename,imageDescription,useBlurhash, \ inReplyTo, inReplyToAtomUri, subject) +def createReportPost(baseDir: str, + nickname: str, domain: str, port: int,httpPrefix: str, \ + content: str, followersOnly: bool, saveToFile: bool, + clientToServer: bool,\ + attachImageFilename: str,imageDescription: str,useBlurhash: bool, \ + debug: bool,subject=None) -> {}: + """Send a report to moderators + """ + domainFull=domain + if port: + if port!=80 and port!=443: + domainFull=domain+':'+str(port) + + # create the list of moderators from teh moderators file + moderatorsList=[] + moderatorsFile=baseDir+'/accounts/moderators.txt' + if os.path.isfile(moderatorsFile): + with open (moderatorsFile, "r") as fileHandler: + for line in fileHandler: + line=line.strip('\n') + if line.startswith('#'): + continue + if line.startswith('/users/'): + line=line.replace('users','') + if line.startswith('@'): + line=line[1:] + if '@' in line: + moderatorActor=httpPrefix+'://'+domainFull+'/users/'+line.split('@')[0] + if moderatorActor not in moderatorList: + moderatorsList.append(moderatorActor) + continue + if line.startswith('http') or line.startswith('dat'): + # must be a local address - no remote moderators + if '://'+domainFull+'/' in line: + if line not in moderatorsList: + moderatorsList.append(line) + else: + if '/' not in line: + moderatorActor=httpPrefix+'://'+domainFull+'/users/'+line + if moderatorActor not in moderatorsList: + moderatorsList.append(moderatorActor) + if len(moderatorsList)==0: + # if there are no moderators then the admin becomes the moderator + adminNickname=getConfigParam(baseDir,'admin') + if adminNickname: + moderatorsList.append(httpPrefix+'://'+domainFull+'/users/'+adminNickname) + if not moderatorsList: + return None + if debug: + print('DEBUG: Sending report to moderators') + print(str(moderatorsList)) + postTo=moderatorsList + postCc=None + return createPostBase(baseDir,nickname, domain, port, \ + postTo,postCc, \ + httpPrefix, content, followersOnly, saveToFile, \ + clientToServer, \ + attachImageFilename,imageDescription,useBlurhash, \ + None, None, subject) + def threadSendPost(session,postJsonObject: {},federationList: [],\ inboxUrl: str, baseDir: str,signatureHeaderJson: {},postLog: [], debug :bool) -> None: diff --git a/roles.py b/roles.py index 8505862a..de60d947 100644 --- a/roles.py +++ b/roles.py @@ -16,6 +16,39 @@ from session import postJson from utils import getNicknameFromActor from utils import getDomainFromActor +def addModerator(baseDir: str,nickname: str): + """Adds a moderator nickname to the file + """ + moderatorsFile=baseDir+'/accounts/moderators.txt' + if os.path.isfile(moderatorsFile): + # is this nickname already in the file? + with open(moderatorsFile, "r") as f: + lines = f.readlines() + for moderator in lines: + moderator=moderator.strip('\n') + if line==nickname: + return + lines.append(nickname) + with open(moderatorsFile, "w") as f: + for moderator in lines: + moderator=moderator.strip('\n') + if len(moderator)>1: + f.write(moderator+'\n') + +def removeModerator(baseDir: str,nickname: str): + """Removes a moderator nickname from the file + """ + moderatorsFile=baseDir+'/accounts/moderators.txt' + if not os.path.isfile(moderatorsFile): + return + with open(moderatorsFile, "r") as f: + lines = f.readlines() + with open(moderatorsFile, "w") as f: + for moderator in lines: + moderator=moderator.strip('\n') + if len(moderator)>1 and moderator!=nickname: + f.write(moderator+'\n') + def setRole(baseDir: str,nickname: str,domain: str, \ project: str,role: str) -> bool: """Set a person's role within a project @@ -30,12 +63,18 @@ def setRole(baseDir: str,nickname: str,domain: str, \ with open(actorFilename, 'r') as fp: actorJson=commentjson.load(fp) if role: + # add the role + if project=='instance' and 'role'=='moderator': + addModerator(baseDir,nickname) if actorJson['roles'].get(project): if role not in actorJson['roles'][project]: actorJson['roles'][project].append(role) else: actorJson['roles'][project]=[role] else: + # remove the role + if project=='instance': + removeModerator(baseDir,nickname) if actorJson['roles'].get(project): actorJson['roles'][project].remove(role) # if the project contains no roles then remove it diff --git a/webinterface.py b/webinterface.py index 575cc2bb..b1904d07 100644 --- a/webinterface.py +++ b/webinterface.py @@ -319,11 +319,16 @@ def htmlTermsOfService(baseDir: str,httpPrefix: str,domainFull: str) -> str: def htmlNewPost(baseDir: str,path: str,inReplyTo: str,mentions: []) -> str: replyStr='' if not path.endswith('/newshare'): - if not inReplyTo: - newPostText='

Enter your post text below.

' + if not path.endswith('/newreport'): + if not inReplyTo: + newPostText='

Enter your post text below.

' + else: + newPostText='

Enter your reply to this post below.

' + replyStr='' else: - newPostText='

Enter your reply to this post below.

' - replyStr='' + newPostText= \ + '

Enter your report below.

' \ + '

This message only goes to moderators, even if it mentions other fediverse addresses.

' else: newPostText='

Enter the details for your shared item below.

' @@ -334,7 +339,7 @@ def htmlNewPost(baseDir: str,path: str,inReplyTo: str,mentions: []) -> str: with open(baseDir+'/epicyon-profile.css', 'r') as cssFile: newPostCSS = cssFile.read() - pathBase=path.replace('/newpost','').replace('/newshare','').replace('/newunlisted','').replace('/newfollowers','').replace('/newdm','') + pathBase=path.replace('/newreport','').replace('/newpost','').replace('/newshare','').replace('/newunlisted','').replace('/newfollowers','').replace('/newdm','') scopeIcon='scope_public.png' scopeDescription='Public' @@ -354,6 +359,10 @@ def htmlNewPost(baseDir: str,path: str,inReplyTo: str,mentions: []) -> str: scopeIcon='scope_dm.png' scopeDescription='Direct Message' endpoint='newdm' + if path.endswith('/newreport'): + scopeIcon='scope_report.png' + scopeDescription='Report' + endpoint='newreport' if path.endswith('/newshare'): scopeIcon='scope_share.png' scopeDescription='Shared Item' @@ -399,7 +408,8 @@ def htmlNewPost(baseDir: str,path: str,inReplyTo: str,mentions: []) -> str: ' Public
Visible to anyone
' \ ' Unlisted
Not on public timeline
' \ ' Followers Only
Only to followers
' \ - ' Direct Message
Only to mentioned people
'+ \ + ' Direct Message
Only to mentioned people
' \ + ' Report
Send to moderators
'+ \ shareOptionOnDropdown+ \ ' ' \ ' ' \