From 7a891e4cd25edcfe349cde041aa5472245c5bb90 Mon Sep 17 00:00:00 2001
From: Bob Mottram <bob@freedombone.net>
Date: Fri, 12 Jul 2019 10:10:09 +0100
Subject: [PATCH] Undoing likes

---
 inbox.py | 63 ++++++++++++++++++++++++++++++++++++++
 like.py  | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 153 insertions(+), 2 deletions(-)

diff --git a/inbox.py b/inbox.py
index bebaef286..e87272c29 100644
--- a/inbox.py
+++ b/inbox.py
@@ -33,6 +33,7 @@ from capabilities import getOcapFilename
 from capabilities import CapablePost
 from capabilities import capabilitiesReceiveUpdate
 from like import updateLikesCollection
+from like import undoLikesCollectionEntry
 
 def getPersonPubKey(session,personUrl: str,personCache: {},debug: bool) -> str:
     if not personUrl:
@@ -439,6 +440,55 @@ def receiveLike(session,handle: str,baseDir: str, \
     updateLikesCollection(postFilename,messageJson['object'],messageJson['actor'],debug)
     return True
 
+def receiveUndoLike(session,handle: str,baseDir: str, \
+                    httpPrefix: str,domain :str,port: int, \
+                    sendThreads: [],postLog: [],cachedWebfingers: {}, \
+                    personCache: {},messageJson: {},federationList: [], \
+                    debug : bool) -> bool:
+    """Receives an undo like activity within the POST section of HTTPServer
+    """
+    if messageJson['type']!='Undo':
+        return False
+    if not messageJson.get('actor'):
+        return False
+    if not messageJson.get('object'):
+        return False
+    if not isinstance(messageJson['object'], dict):
+        return False
+    if not messageJson['object'].get('type'):
+        return False
+    if messageJson['object']['type']!='Like':
+        return False
+    if not messageJson['object'].get('object'):
+        if debug:
+            print('DEBUG: '+messageJson['type']+' like has no object')
+        return False
+    if not isinstance(messageJson['object']['object'], str):
+        if debug:
+            print('DEBUG: '+messageJson['type']+' like object is not a string')
+        return False
+    if '/users/' not in messageJson['actor']:
+        if debug:
+            print('DEBUG: "users" missing from actor in '+messageJson['type']+' like')
+        return False
+    if '/statuses/' not in messageJson['object']['object']:
+        if debug:
+            print('DEBUG: "statuses" missing from like object in '+messageJson['type'])
+        return False
+    if not os.path.isdir(baseDir+'/accounts/'+handle):
+        print('DEBUG: unknown recipient of undo like - '+handle)
+    # if this post in the outbox of the person?
+    postFilename=locatePost(baseDir,handle.split('@')[0],handle.split('@')[1],messageJson['object']['object'])
+    if not postFilename:
+        if debug:
+            print('DEBUG: post not found in inbox or outbox')
+            print(messageJson['object']['object'])
+        return True
+    if debug:
+        print('DEBUG: liked post found in inbox. Now undoing.')
+    undoLikesCollectionEntry(postFilename,messageJson['object'],messageJson['actor'],debug)
+    return True
+
 def receiveDelete(session,handle: str,baseDir: str, \
                   httpPrefix: str,domain :str,port: int, \
                   sendThreads: [],postLog: [],cachedWebfingers: {}, \
@@ -557,6 +607,19 @@ def inboxAfterCapabilities(session,keyId: str,handle: str,messageJson: {}, \
             print('DEBUG: Like accepted from '+keyId)
         return False
 
+    if receiveUndoLike(session,handle, \
+                       baseDir,httpPrefix, \
+                       domain,port, \
+                       sendThreads,postLog, \
+                       cachedWebfingers, \
+                       personCache, \
+                       messageJson, \
+                       federationList, \
+                       debug):
+        if debug:
+            print('DEBUG: Undo like accepted from '+keyId)
+        return False
+
     if receiveAnnounce(session,handle, \
                        baseDir,httpPrefix, \
                        domain,port, \
diff --git a/like.py b/like.py
index 78d80ae90..d19edde49 100644
--- a/like.py
+++ b/like.py
@@ -103,8 +103,9 @@ def like(session,baseDir: str,federationList: [],nickname: str,domain: str,port:
          sendThreads: [],postLog: [],personCache: {},cachedWebfingers: {}, \
          debug: bool) -> {}:
     """Creates a like
-    ccUrl might be a specific person whose post was liked
-    objectUrl is typically the url of the message, corresponding to url or atomUri in createPostBase
+    actor is the person doing the liking
+    'to' might be a specific person (actor) whose post was liked
+    object is typically the url of the message which was liked
     """
     if not urlPermitted(objectUrl,federationList,"inbox:write"):
         return None
@@ -176,3 +177,90 @@ def likePost(session,baseDir: str,federationList: [], \
                 ccList,httpPrefix,objectUrl,clientToServer, \
                 sendThreads,postLog,personCache,cachedWebfingers,debug)
 
+def undolike(session,baseDir: str,federationList: [],nickname: str,domain: str,port: int, \
+             ccList: [],httpPrefix: str,objectUrl: str,clientToServer: bool, \
+             sendThreads: [],postLog: [],personCache: {},cachedWebfingers: {}, \
+             debug: bool) -> {}:
+    """Removes a like
+    actor is the person doing the liking
+    'to' might be a specific person (actor) whose post was liked
+    object is typically the url of the message which was liked
+    """
+    if not urlPermitted(objectUrl,federationList,"inbox:write"):
+        return None
+
+    fullDomain=domain
+    if port!=80 and port!=443:
+        if ':' not in domain:
+            fullDomain=domain+':'+str(port)
+
+    newUndoLikeJson = {
+        'type': 'Undo',
+        'actor': httpPrefix+'://'+fullDomain+'/users/'+nickname,
+        'object': {
+            'type': 'Like',
+            'actor': httpPrefix+'://'+fullDomain+'/users/'+nickname,
+            'object': objectUrl,
+            'to': [httpPrefix+'://'+fullDomain+'/users/'+nickname+'/followers'],
+            'cc': []
+        },
+        'to': [httpPrefix+'://'+fullDomain+'/users/'+nickname+'/followers'],
+        'cc': []
+    }
+    if ccList:
+        if len(ccList)>0:
+            newUndoLikeJson['cc']=ccList
+            newUndoLikeJson['object']['cc']=ccList
+
+    # Extract the domain and nickname from a statuses link
+    likedPostNickname=None
+    likedPostDomain=None
+    likedPostPort=None
+    if '/users/' in objectUrl:
+        likedPostNickname=getNicknameFromActor(objectUrl)
+        likedPostDomain,likedPostPort=getDomainFromActor(objectUrl)
+
+    if likedPostNickname:
+        postFilename=locatePost(baseDir,nickname,domain,objectUrl)
+        if not postFilename:
+            return None
+
+        undoLikesCollectionEntry(postFilename,objectUrl,newLikeJson['actor'],debug)
+        
+        sendSignedJson(newLikeJson,session,baseDir, \
+                       nickname,domain,port, \
+                       likedPostNickname,likedPostDomain,likedPostPort, \
+                       'https://www.w3.org/ns/activitystreams#Public', \
+                       httpPrefix,True,clientToServer,federationList, \
+                       sendThreads,postLog,cachedWebfingers,personCache,debug)
+    else:
+        return None
+
+    return newLikeJson
+
+def undoLikePost(session,baseDir: str,federationList: [], \
+                 nickname: str,domain: str,port: int,httpPrefix: str, \
+                 likeNickname: str,likeDomain: str,likePort: int, \
+                 ccList: [], \
+                 likeStatusNumber: int,clientToServer: bool, \
+                 sendThreads: [],postLog: [], \
+                 personCache: {},cachedWebfingers: {}, \
+                 debug: bool) -> {}:
+    """Removes a liked post
+    """
+    likeDomain=likeDomain
+    if likePort!=80 and likePort!=443:
+        likeDomain=likeDomain+':'+str(likePort)
+
+    objectUrl = \
+        httpPrefix + '://'+likeDomain+'/users/'+likeNickname+ \
+        '/statuses/'+str(likeStatusNumber)
+
+    if likePort!=80 and likePort!=443:
+        ccUrl=httpPrefix+'://'+likeDomain+':'+str(likePort)+'/users/'+likeNickname
+    else:
+        ccUrl=httpPrefix+'://'+likeDomain+'/users/'+likeNickname
+        
+    return undoLike(session,baseDir,federationList,nickname,domain,port, \
+                    ccList,httpPrefix,objectUrl,clientToServer, \
+                    sendThreads,postLog,personCache,cachedWebfingers,debug)