diff --git a/daemon.py b/daemon.py index 1f9e7bae..18f4c711 100644 --- a/daemon.py +++ b/daemon.py @@ -35,6 +35,7 @@ from auth import createPassword from threads import threadWithTrace from media import getMediaPath from media import createMediaDirs +from delete import outboxDelete import os import sys @@ -179,6 +180,7 @@ class PubServer(BaseHTTPRequestHandler): postId=None if self.server.debug: pprint(messageJson) + print('DEBUG: savePostToBox') savePostToBox(self.server.baseDir, \ self.server.httpPrefix, \ postId, \ @@ -204,6 +206,9 @@ class PubServer(BaseHTTPRequestHandler): if self.server.debug: print('DEBUG: handle any unfollow requests') outboxUndoFollow(self.server.baseDir,messageJson,self.server.debug) + if self.server.debug: + print('DEBUG: handle delete requests') + outboxDelete(self.server.baseDir,self.server.httpPrefix,messageJson,self.server.debug) if self.server.debug: print('DEBUG: sending c2s post to named addresses') print('c2s sender: '+self.postToNickname+'@'+self.server.domain+':'+str(self.server.port)) diff --git a/delete.py b/delete.py index 266a0974..24fc6327 100644 --- a/delete.py +++ b/delete.py @@ -14,7 +14,13 @@ from utils import createOutboxDir from utils import urlPermitted from utils import getNicknameFromActor from utils import getDomainFromActor +from utils import locatePost +from utils import deletePost from posts import sendSignedJson +from session import postJson +from webfinger import webfingerHandle +from auth import createBasicAuthHeader +from posts import getPersonBox def createDelete(session,baseDir: str,federationList: [], \ nickname: str, domain: str, port: int, \ @@ -72,6 +78,73 @@ def createDelete(session,baseDir: str,federationList: [], \ return newDelete +def sendDeleteViaServer(session,fromNickname: str,password: str, + fromDomain: str,fromPort: int, \ + httpPrefix: str,deleteObjectUrl: str, \ + cachedWebfingers: {},personCache: {}, \ + debug: bool) -> {}: + """Creates a delete request message via c2s + """ + if not session: + print('WARN: No session for sendDeleteViaServer') + return 6 + + fromDomainFull=fromDomain + if fromPort!=80 and fromPort!=443: + fromDomainFull=fromDomain+':'+str(fromPort) + + toUrl = 'https://www.w3.org/ns/activitystreams#Public' + ccUrl = httpPrefix + '://'+fromDomainFull+'/users/'+fromNickname+'/followers' + + newDeleteJson = { + 'actor': httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname, + 'cc': [ccUrl], + 'object': deleteObjectUrl, + 'to': [toUrl], + 'type': 'Delete' + } + + handle=httpPrefix+'://'+fromDomainFull+'/@'+fromNickname + + # lookup the inbox for the To handle + wfRequest = webfingerHandle(session,handle,httpPrefix,cachedWebfingers) + if not wfRequest: + if debug: + print('DEBUG: announce webfinger failed for '+handle) + return 1 + + postToBox='outbox' + + # get the actor inbox for the To handle + inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition = \ + getPersonBox(session,wfRequest,personCache,postToBox) + + if not inboxUrl: + if debug: + print('DEBUG: No '+postToBox+' was found for '+handle) + return 3 + if not fromPersonId: + if debug: + print('DEBUG: No actor was found for '+handle) + return 4 + + authHeader=createBasicAuthHeader(fromNickname,password) + + headers = {'host': fromDomain, \ + 'Content-type': 'application/json', \ + 'Authorization': authHeader} + postResult = \ + postJson(session,newDeleteJson,[],inboxUrl,headers,"inbox:write") + #if not postResult: + # if debug: + # print('DEBUG: POST announce failed for c2s to '+inboxUrl) + # return 5 + + if debug: + print('DEBUG: c2s POST delete request success') + + return newDeleteJson + def deletePublic(session,baseDir: str,federationList: [], \ nickname: str, domain: str, port: int, httpPrefix: str, \ objectUrl: str,clientToServer: bool, \ @@ -95,14 +168,14 @@ def deletePublic(session,baseDir: str,federationList: [], \ personCache,cachedWebfingers, \ debug) -def deletePost(session,baseDir: str,federationList: [], \ - nickname: str, domain: str, port: int, httpPrefix: str, \ - deleteNickname: str, deleteDomain: str, \ - deletePort: int, deleteHttpsPrefix: str, \ - deleteStatusNumber: int,clientToServer: bool, \ - sendThreads: [],postLog: [], \ - personCache: {},cachedWebfingers: {}, \ - debug: bool) -> {}: +def deletePostPub(session,baseDir: str,federationList: [], \ + nickname: str, domain: str, port: int, httpPrefix: str, \ + deleteNickname: str, deleteDomain: str, \ + deletePort: int, deleteHttpsPrefix: str, \ + deleteStatusNumber: int,clientToServer: bool, \ + sendThreads: [],postLog: [], \ + personCache: {},cachedWebfingers: {}, \ + debug: bool) -> {}: """Deletes a given status post """ deletedDomain=deleteDomain @@ -120,3 +193,45 @@ def deletePost(session,baseDir: str,federationList: [], \ personCache,cachedWebfingers, \ debug) +def outboxDelete(baseDir: str,httpPrefix: str,messageJson: {},debug: bool) -> None: + """When a delete request is received by the outbox from c2s + """ + if not messageJson.get('type'): + if debug: + print('DEBUG: delete - no type') + return + if not messageJson['type']=='Delete': + if debug: + print('DEBUG: not a delete') + return + if not messageJson.get('object'): + if debug: + print('DEBUG: no object in delete') + return + if not isinstance(messageJson['object'], str): + if debug: + print('DEBUG: delete object is not string') + return + if debug: + print('DEBUG: c2s delete request arrived in outbox') + + messageId=messageJson['object'].replace('/activity','') + if '/statuses/' not in messageId: + if debug: + print('DEBUG: c2s delete object is not a status') + return + if '/users/' not in messageId: + if debug: + print('DEBUG: c2s delete object has no nickname') + return + deleteNickname=getNicknameFromActor(messageId) + deleteDomain,deletePort=getDomainFromActor(messageId) + postFilename=locatePost(baseDir,deleteNickname,deleteDomain,messageId) + if not postFilename: + if debug: + print('DEBUG: c2s delete post not found in inbox or outbox') + print(messageId) + return True + deletePost(baseDir,httpPrefix,deleteNickname,deleteDomain,postFilename,debug) + if debug: + print('DEBUG: post deleted via c2s - '+postFilename) diff --git a/epicyon.py b/epicyon.py index a4a93a50..5db75a68 100644 --- a/epicyon.py +++ b/epicyon.py @@ -62,6 +62,7 @@ from auth import createPassword from utils import getDomainFromActor from utils import getNicknameFromActor from media import archiveMedia +from delete import sendDeleteViaServer import argparse def str2bool(v): @@ -218,8 +219,8 @@ if args.tests: if args.testsnetwork: print('Network Tests') - testPostMessageBetweenServers() - testFollowBetweenServers() + #testPostMessageBetweenServers() + #testFollowBetweenServers() testClientToServer() sys.exit() diff --git a/inbox.py b/inbox.py index 3e0e67ed..ec1803c3 100644 --- a/inbox.py +++ b/inbox.py @@ -623,6 +623,8 @@ def receiveDelete(session,handle: str,baseDir: str, \ if debug: print('DEBUG: '+messageJson['type']+' has no actor') return False + if debug: + print('DEBUG: Delete activity arrived') if not messageJson.get('object'): if debug: print('DEBUG: '+messageJson['type']+' has no object') @@ -649,11 +651,12 @@ def receiveDelete(session,handle: str,baseDir: str, \ if not os.path.isdir(baseDir+'/accounts/'+handle): print('DEBUG: unknown recipient of like - '+handle) # if this post in the outbox of the person? - postFilename=locatePost(baseDir,handle.split('@')[0],handle.split('@')[1],messageJson['object']) + messageId=messageJson['object'].replace('/activity','') + postFilename=locatePost(baseDir,handle.split('@')[0],handle.split('@')[1],messageId) if not postFilename: if debug: print('DEBUG: delete post not found in inbox or outbox') - print(messageJson['object']) + print(messageId) return True deletePost(baseDir,httpPrefix,handle.split('@')[0],handle.split('@')[1],postFilename,debug) if debug: diff --git a/tests.py b/tests.py index b551e5d3..69ceac28 100644 --- a/tests.py +++ b/tests.py @@ -52,6 +52,7 @@ from like import likePost from announce import announcePublic from announce import sendAnnounceViaServer from media import getMediaPath +from delete import sendDeleteViaServer testServerAliceRunning = False testServerBobRunning = False @@ -1091,14 +1092,37 @@ def testClientToServer(): assert os.path.isfile(aliceDir+'/accounts/alice@'+aliceDomain+'/following.txt') assert 'alice@'+aliceDomain+':'+str(alicePort) in open(bobDir+'/accounts/bob@'+bobDomain+'/followers.txt').read() assert 'bob@'+bobDomain+':'+str(bobPort) in open(aliceDir+'/accounts/alice@'+aliceDomain+'/following.txt').read() + + print('\n\nBob follows Alice') + sendFollowRequestViaServer(sessionAlice,'bob','bobpass', \ + bobDomain,bobPort, \ + 'alice',aliceDomain,alicePort, \ + httpPrefix, \ + cachedWebfingers,personCache, \ + True) + for t in range(10): + if os.path.isfile(aliceDir+'/accounts/alice@'+aliceDomain+'/followers.txt'): + if 'bob@'+bobDomain+':'+str(bobPort) in open(aliceDir+'/accounts/alice@'+aliceDomain+'/followers.txt').read(): + if os.path.isfile(bobDir+'/accounts/bob@'+bobDomain+'/following.txt'): + if 'alice@'+aliceDomain+':'+str(alicePort) in open(bobDir+'/accounts/bob@'+bobDomain+'/following.txt').read(): + break + time.sleep(1) + + assert os.path.isfile(aliceDir+'/accounts/alice@'+aliceDomain+'/followers.txt') + assert os.path.isfile(bobDir+'/accounts/bob@'+bobDomain+'/following.txt') + assert 'bob@'+bobDomain+':'+str(bobPort) in open(aliceDir+'/accounts/alice@'+aliceDomain+'/followers.txt').read() + assert 'alice@'+aliceDomain+':'+str(alicePort) in open(bobDir+'/accounts/bob@'+bobDomain+'/following.txt').read() + print('\n\nBob repeats the post') sessionBob = createSession(bobDomain,bobPort,useTor) password='bobpass' outboxPath=bobDir+'/accounts/bob@'+bobDomain+'/outbox' inboxPath=aliceDir+'/accounts/alice@'+aliceDomain+'/inbox' - assert len([name for name in os.listdir(outboxPath) if os.path.isfile(os.path.join(outboxPath, name))])==0 - assert len([name for name in os.listdir(inboxPath) if os.path.isfile(os.path.join(inboxPath, name))])==0 + print(str(len([name for name in os.listdir(outboxPath) if os.path.isfile(os.path.join(outboxPath, name))]))) + assert len([name for name in os.listdir(outboxPath) if os.path.isfile(os.path.join(outboxPath, name))])==1 + print(str(len([name for name in os.listdir(inboxPath) if os.path.isfile(os.path.join(inboxPath, name))]))) + assert len([name for name in os.listdir(inboxPath) if os.path.isfile(os.path.join(inboxPath, name))])==1 sendAnnounceViaServer(sessionBob,'bob',password, \ bobDomain,bobPort, \ httpPrefix,outboxPostId, \ @@ -1106,16 +1130,36 @@ def testClientToServer(): personCache,True) for i in range(20): if os.path.isdir(outboxPath) and os.path.isdir(inboxPath): - if len([name for name in os.listdir(outboxPath) if os.path.isfile(os.path.join(outboxPath, name))])==1: - if len([name for name in os.listdir(inboxPath) if os.path.isfile(os.path.join(inboxPath, name))])==1: + if len([name for name in os.listdir(outboxPath) if os.path.isfile(os.path.join(outboxPath, name))])==2: + if len([name for name in os.listdir(inboxPath) if os.path.isfile(os.path.join(inboxPath, name))])==2: break time.sleep(1) - assert len([name for name in os.listdir(outboxPath) if os.path.isfile(os.path.join(outboxPath, name))])==1 - assert len([name for name in os.listdir(inboxPath) if os.path.isfile(os.path.join(inboxPath, name))])==1 + assert len([name for name in os.listdir(outboxPath) if os.path.isfile(os.path.join(outboxPath, name))])==2 + assert len([name for name in os.listdir(inboxPath) if os.path.isfile(os.path.join(inboxPath, name))])==2 print('Post repeated') + inboxPath=bobDir+'/accounts/bob@'+bobDomain+'/inbox' + outboxPath=aliceDir+'/accounts/alice@'+aliceDomain+'/outbox' + postsBefore = len([name for name in os.listdir(inboxPath) if os.path.isfile(os.path.join(inboxPath, name))]) + print('\n\nAlice deletes her post: '+outboxPostId+' '+str(postsBefore)) + password='alicepass' + sendDeleteViaServer(sessionAlice,'alice',password, + aliceDomain,alicePort, \ + httpPrefix,outboxPostId, \ + cachedWebfingers,personCache, \ + True) + for i in range(30): + if os.path.isdir(inboxPath): + if len([name for name in os.listdir(inboxPath) if os.path.isfile(os.path.join(inboxPath, name))])==postsBefore-1: + break + time.sleep(1) + + assert len([name for name in os.listdir(inboxPath) if os.path.isfile(os.path.join(inboxPath, name))])==postsBefore-1 + print(">>> post deleted from Alice's outbox and Bob's inbox") + + print('\n\nAlice unfollows Bob') password='alicepass' sendUnfollowRequestViaServer(sessionAlice,'alice',password, \