From c55b5c9e153f23939b9227ecb46e2505d4a883c3 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 17 Jul 2019 11:34:00 +0100 Subject: [PATCH] unfollowing functions --- daemon.py | 4 ++ follow.py | 144 ++++++++++++++++++++++++++++++++++++++++++++++++++---- inbox.py | 91 ++++++++++++++++++++++++++++++++++ 3 files changed, 229 insertions(+), 10 deletions(-) diff --git a/daemon.py b/daemon.py index 96094945..1f9e7bae 100644 --- a/daemon.py +++ b/daemon.py @@ -29,6 +29,7 @@ from inbox import inboxMessageHasParams from inbox import runInboxQueue from inbox import savePostToInboxQueue from follow import getFollowingFeed +from follow import outboxUndoFollow from auth import authorize from auth import createPassword from threads import threadWithTrace @@ -200,6 +201,9 @@ class PubServer(BaseHTTPRequestHandler): self.server.cachedWebfingers, \ self.server.personCache, \ messageJson,self.server.debug) + if self.server.debug: + print('DEBUG: handle any unfollow requests') + outboxUndoFollow(self.server.baseDir,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/follow.py b/follow.py index 818b7456..76c41a0e 100644 --- a/follow.py +++ b/follow.py @@ -62,7 +62,7 @@ def followerOfPerson(baseDir: str,nickname: str, domain: str, \ def unfollowPerson(baseDir: str,nickname: str, domain: str, \ followNickname: str, followDomain: str, \ - followFile='following.txt') -> None: + followFile='following.txt') -> bool: """Removes a person to the follow list """ handle=nickname.lower()+'@'+domain.lower() @@ -72,15 +72,16 @@ def unfollowPerson(baseDir: str,nickname: str, domain: str, \ if not os.path.isdir(baseDir+'/accounts/'+handle): os.mkdir(baseDir+'/accounts/'+handle) filename=baseDir+'/accounts/'+handle+'/'+followFile - if os.path.isfile(filename): - if handleToUnfollow not in open(filename).read(): - return - with open(filename, "r") as f: - lines = f.readlines() - with open(filename, "w") as f: - for line in lines: - if line.strip("\n") != handleToUnfollow: - f.write(line) + if not os.path.isfile(filename): + return False + if handleToUnfollow not in open(filename).read(): + return + with open(filename, "r") as f: + lines = f.readlines() + with open(filename, "w") as f: + for line in lines: + if line.strip("\n") != handleToUnfollow: + f.write(line) def unfollowerOfPerson(baseDir: str,nickname: str,domain: str, \ followerNickname: str,followerDomain: str) -> None: @@ -432,6 +433,81 @@ def sendFollowRequestViaServer(session,fromNickname: str,password: str, return newFollowJson +def sendUnfollowRequestViaServer(session,fromNickname: str,password: str, + fromDomain: str,fromPort: int, \ + followNickname: str,followDomain: str,followPort: int, \ + httpPrefix: str, \ + cachedWebfingers: {},personCache: {}, \ + debug: bool) -> {}: + """Creates a unfollow request via c2s + """ + if not session: + print('WARN: No session for sendUnfollowRequestViaServer') + return 6 + + fromDomainFull=fromDomain + if fromPort!=80 and fromPort!=443: + fromDomainFull=fromDomain+':'+str(fromPort) + followDomainFull=followDomain + if followPort!=80 and followPort!=443: + followDomainFull=followDomain+':'+str(followPort) + + followActor=httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname + followedId=httpPrefix+'://'+followDomainFull+'/users/'+followNickname + + unfollowJson = { + 'type': 'Undo', + 'actor': followActor, + 'object': { + 'type': 'Follow', + 'actor': followActor, + 'object': followedId, + 'to': [followedId], + 'cc': ['https://www.w3.org/ns/activitystreams#Public'] + } + } + + 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,unfollowJson,[],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 unfollow success') + + return unfollowJson + def getFollowersOfActor(baseDir :str,actor :str,debug: bool) -> {}: """In a shared inbox if we receive a post we know who it's from and if it's addressed to followers then we need to get a list of those. @@ -493,3 +569,51 @@ def getFollowersOfActor(baseDir :str,actor :str,debug: bool) -> {}: print(ocapFilename) recipientsDict[account]=None return recipientsDict + +def outboxUndoFollow(baseDir: str,messageJson: {},debug: bool) -> None: + """When an unfollow request is received by the outbox from c2s + This removes the followed handle from the following.txt file + of the relevant account + """ + if not messageJson.get('type'): + return + if not messageJson['type']=='Undo': + return + if not messageJson.get('object'): + return + if not isinstance(messageJson['object'], dict): + return + if not messageJson['object'].get('type'): + return + if not messageJson['object']['type']=='Follow': + return + if not messageJson['object'].get('object'): + return + if not messageJson['object'].get('actor'): + return + if not isinstance(messageJson['object']['object'], str): + return + if debug: + print('DEBUG: undo follow arrived in outbox') + + nicknameFollower=getNicknameFromActor(messageJson['object']['actor']) + domainFollower,portFollower=getDomainFromActor(messageJson['object']['actor']) + domainFollowerFull=domainFollower + if portFollower: + if portFollower!=80 and portFollower!=443: + domainFollowerFull=domainFollower+':'+str(portFollower) + + nicknameFollowing=getNicknameFromActor(messageJson['object']['object']) + domainFollowing,portFollowing=getDomainFromActor(messageJson['object']['object']) + domainFollowingFull=domainFollowing + if portFollowing: + if portFollowing!=80 and portFollowing!=443: + domainFollowingFull=domainFollowing+':'+str(portFollowing) + + if unfollowPerson(baseDir,nicknameFollower,domainFollowerFull, \ + nicknameFollowing,domainFollowingFull): + if debug: + print('DEBUG: '+nicknameFollower+' unfollowed '+nicknameFollowing+'@'+domainFollowingFull) + else: + if debug: + print('WARN: '+nicknameFollower+' could not unfollow '+nicknameFollowing+'@'+domainFollowingFull) diff --git a/inbox.py b/inbox.py index f47f45bc..7bbe4253 100644 --- a/inbox.py +++ b/inbox.py @@ -379,6 +379,82 @@ def inboxPostRecipients(baseDir :str,postJsonObject :{},httpPrefix :str,domain : return recipientsDict,recipientsDictFollowers +def receiveUndoFollow(session,baseDir: str,httpPrefix: str, \ + port: int,messageJson: {},debug : bool) -> bool: + if not messageJson['object'].get('actor'): + if debug: + print('DEBUG: follow request has no actor within object') + return False + if '/users/' not in messageJson['object']['actor']: + if debug: + print('DEBUG: "users" missing from actor within object') + return False + if messageJson['object']['actor'] != messageJson['actor']: + if debug: + print('DEBUG: actors do not match') + return False + + nicknameFollower=getNicknameFromActor(messageJson['object']['actor']) + domainFollower,portFollower=getDomainFromActor(messageJson['object']['actor']) + domainFollowerFull=domainFollower + if portFollower: + if portFollower!=80 and portFollower!=443: + domainFollowerFull=domainFollower+':'+str(portFollower) + + nicknameFollowing=getNicknameFromActor(messageJson['object']['object']) + domainFollowing,portFollowing=getDomainFromActor(messageJson['object']['object']) + domainFollowingFull=domainFollowing + if portFollowing: + if portFollowing!=80 and portFollowing!=443: + domainFollowingFull=domainFollowing+':'+str(portFollowing) + + unfollowerOfPerson(baseDir,nicknameFollower,domainFollowerFull, \ + nicknameFollowing,domainFollowingFull,federationList,debug) + return True + +def receiveUndo(session,baseDir: str,httpPrefix: str, \ + port: int,sendThreads: [],postLog: [], \ + cachedWebfingers: {},personCache: {}, \ + messageJson: {},federationList: [], \ + debug : bool, \ + acceptedCaps=["inbox:write","objects:read"]) -> bool: + """Receives an undo request within the POST section of HTTPServer + """ + if not messageJson['type'].startswith('Undo'): + return False + if not messageJson.get('actor'): + if debug: + print('DEBUG: follow request has no actor') + return False + if '/users/' not in messageJson['actor']: + if debug: + print('DEBUG: "users" missing from actor') + return False + if not messageJson.get('object'): + if debug: + print('DEBUG: '+messageJson['type']+' has no object') + return False + if not isinstance(messageJson['object'], dict): + if debug: + print('DEBUG: '+messageJson['type']+' object is not a dict') + return False + if not messageJson['object'].get('type'): + if debug: + print('DEBUG: '+messageJson['type']+' has no object type') + return False + if not messageJson['object'].get('object'): + if debug: + print('DEBUG: '+messageJson['type']+' has no object within object') + return False + if not isinstance(messageJson['object']['object'], str): + if debug: + print('DEBUG: '+messageJson['type']+' object within object is not a string') + return False + if messageJson['object']['type']=='Follow': + return receiveUndoFollow(session,baseDir,httpPrefix, \ + port,messageJson,debug) + return False + def receiveUpdate(session,baseDir: str, \ httpPrefix: str,domain :str,port: int, \ sendThreads: [],postLog: [],cachedWebfingers: {}, \ @@ -953,6 +1029,21 @@ def runInboxQueue(baseDir: str,httpPrefix: str,sendThreads: [],postLog: [], \ if debug: print('DEBUG: Signature check success') + if receiveUndo(session, \ + baseDir,httpPrefix,port, \ + sendThreads,postLog, \ + cachedWebfingers, + personCache, + queueJson['post'], \ + federationList, \ + debug, \ + acceptedCaps=["inbox:write","objects:read"]): + if debug: + print('DEBUG: Undo accepted from '+keyId) + os.remove(queueFilename) + queue.pop(0) + continue + if receiveFollowRequest(session, \ baseDir,httpPrefix,port, \ sendThreads,postLog, \