From ab475f05f6c933b6539d3fdd9b81a08ff81b4f78 Mon Sep 17 00:00:00 2001
From: Bob Mottram <bob@freedombone.net>
Date: Thu, 11 Jul 2019 22:38:28 +0100
Subject: [PATCH] Delete activity

---
 delete.py | 122 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 inbox.py  |  66 ++++++++++++++++++++++++++++-
 2 files changed, 187 insertions(+), 1 deletion(-)
 create mode 100644 delete.py

diff --git a/delete.py b/delete.py
new file mode 100644
index 00000000..ce003af3
--- /dev/null
+++ b/delete.py
@@ -0,0 +1,122 @@
+__filename__ = "delete.py"
+__author__ = "Bob Mottram"
+__license__ = "AGPL3+"
+__version__ = "0.0.1"
+__maintainer__ = "Bob Mottram"
+__email__ = "bob@freedombone.net"
+__status__ = "Production"
+
+import os
+import json
+import commentjson
+from utils import getStatusNumber
+from utils import createOutboxDir
+from utils import urlPermitted
+from utils import getNicknameFromActor
+from utils import getDomainFromActor
+from posts import sendSignedJson
+
+def createDelete(session,baseDir: str,federationList: [], \
+                 nickname: str, domain: str, port: int, \
+                 toUrl: str, ccUrl: str, httpPrefix: str, \
+                 objectUrl: str,clientToServer: bool, \
+                 sendThreads: [],postLog: [], \
+                 personCache: {},cachedWebfingers: {}, \
+                 debug: bool) -> {}:
+    """Creates an delete message
+    Typically toUrl will be https://www.w3.org/ns/activitystreams#Public
+    and ccUrl might be a specific person whose post is to be deleted
+    objectUrl is typically the url of the message, corresponding to url
+    or atomUri in createPostBase
+    """
+    if not urlPermitted(objectUrl,federationList,"inbox:write"):
+        return None
+
+    if ':' in domain:
+        domain=domain.split(':')[0]
+        fullDomain=domain
+    if port!=80 and port!=443:
+        fullDomain=domain+':'+str(port)
+
+    statusNumber,published = getStatusNumber()
+    newDeleteId= \
+        httpPrefix+'://'+fullDomain+'/users/'+nickname+'/statuses/'+statusNumber
+    newDelete = {
+        'actor': httpPrefix+'://'+fullDomain+'/users/'+nickname,
+        'atomUri': httpPrefix+'://'+fullDomain+'/users/'+nickname+'/statuses/'+statusNumber,
+        'cc': [],
+        'id': newDeleteId+'/activity',
+        'object': objectUrl,
+        'published': published,
+        'to': [toUrl],
+        'type': 'Delete'
+    }
+    if ccUrl:
+        if len(ccUrl)>0:
+            newDelete['cc']=[ccUrl]
+
+    deleteNickname=None
+    deleteDomain=None
+    deletePort=None
+    if '/users/' in objectUrl:
+        deleteNickname=getNicknameFromActor(objectUrl)
+        deleteDomain,deletePort=getDomainFromActor(objectUrl)
+
+    if deleteNickname and deleteDomain:
+        sendSignedJson(newDelete,session,baseDir, \
+                       nickname,domain,port, \
+                       deleteNickname,deleteDomain,deletePort, \
+                       'https://www.w3.org/ns/activitystreams#Public', \
+                       httpPrefix,True,clientToServer,federationList, \
+                       sendThreads,postLog,cachedWebfingers,personCache,debug)
+        
+    return newDelete
+
+def deletePublic(session,baseDir: str,federationList: [], \
+                 nickname: str, domain: str, port: int, httpPrefix: str, \
+                 objectUrl: str,clientToServer: bool, \
+                 sendThreads: [],postLog: [], \
+                 personCache: {},cachedWebfingers: {}, \
+                 debug: bool) -> {}:
+    """Makes a public delete activity
+    """
+    fromDomain=domain
+    if port!=80 and port!=443:
+        if ':' not in domain:
+            fromDomain=domain+':'+str(port)
+
+    toUrl = 'https://www.w3.org/ns/activitystreams#Public'
+    ccUrl = httpPrefix + '://'+fromDomain+'/users/'+nickname+'/followers'
+    return createDelete(session,baseDir,federationList, \
+                        nickname,domain,port, \
+                        toUrl,ccUrl,httpPrefix, \
+                        objectUrl,clientToServer, \
+                        sendThreads,postLog, \
+                        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) -> {}:
+    """Deletes a given status post
+    """
+    deletedDomain=deleteDomain
+    if deletePort!=80 and deletePort!=443:
+        if ':' not in deletedDomain:
+            deletedDomain=deletedDomain+':'+str(deletePort)
+
+    objectUrl = deleteHttpsPrefix + '://'+deletedDomain+'/users/'+ \
+        deleteNickname+'/statuses/'+str(deleteStatusNumber)
+
+    return deletePublic(session,baseDir,federationList, \
+                        nickname,domain,port,httpPrefix, \
+                        objectUrl,clientToServer, \
+                        sendThreads,postLog, \
+                        personCache,cachedWebfingers, \
+                        debug)
+
diff --git a/inbox.py b/inbox.py
index a9998440..338fbb10 100644
--- a/inbox.py
+++ b/inbox.py
@@ -90,6 +90,7 @@ def inboxPermittedMessage(domain: str,messageJson: {},federationList: []) -> boo
 
     if messageJson['type']!='Follow' and \
        messageJson['type']!='Like' and \
+       messageJson['type']!='Delete' and \
        messageJson['type']!='Announce':
         if messageJson.get('object'):
             if messageJson['object'].get('inReplyTo'):
@@ -438,6 +439,53 @@ def receiveLike(session,handle: str,baseDir: str, \
     updateLikesCollection(postFilename,messageJson['object'],messageJson['actor'],debug)
     return True
 
+def receiveDelete(session,handle: str,baseDir: str, \
+                  httpPrefix: str,domain :str,port: int, \
+                  sendThreads: [],postLog: [],cachedWebfingers: {}, \
+                  personCache: {},messageJson: {},federationList: [], \
+                  debug : bool) -> bool:
+    """Receives a Delete activity within the POST section of HTTPServer
+    """
+    if messageJson['type']!='Delete':
+        return False
+    if not messageJson.get('actor'):
+        if debug:
+            print('DEBUG: '+messageJson['type']+' has no actor')
+        return False
+    if not messageJson.get('object'):
+        if debug:
+            print('DEBUG: '+messageJson['type']+' has no object')
+        return False
+    if not isinstance(messageJson['object'], str):
+        if debug:
+            print('DEBUG: '+messageJson['type']+' object is not a string')
+        return False
+    if not messageJson.get('to'):
+        if debug:
+            print('DEBUG: '+messageJson['type']+' has no "to" list')
+        return False
+    if '/users/' not in messageJson['actor']:
+        if debug:
+            print('DEBUG: "users" missing from actor in '+messageJson['type'])
+        return False
+    if '/statuses/' not in messageJson['object']:
+        if debug:
+            print('DEBUG: "statuses" missing from object in '+messageJson['type'])
+        return False
+    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'])
+    if not postFilename:
+        if debug:
+            print('DEBUG: delete post not found in inbox or outbox')
+            print(messageJson['object'])
+        return True
+    os.remove(postFilename)
+    if debug:
+        print('DEBUG: post deleted - '+postFilename)
+    return True
+
 def receiveAnnounce(session,handle: str,baseDir: str, \
                     httpPrefix: str,domain :str,port: int, \
                     sendThreads: [],postLog: [],cachedWebfingers: {}, \
@@ -518,6 +566,18 @@ def inboxAfterCapabilities(session,keyId: str,handle: str,messageJson: {}, \
         if debug:
             print('DEBUG: Announce accepted from '+keyId)
 
+    if receiveDelete(session,handle, \
+                     baseDir,httpPrefix, \
+                     domain,port, \
+                     sendThreads,postLog, \
+                     cachedWebfingers, \
+                     personCache, \
+                     messageJson, \
+                     federationList, \
+                     debug):
+        if debug:
+            print('DEBUG: Announce accepted from '+keyId)
+
     if debug:
         print('DEBUG: object capabilities passed')
         print('copy from '+queueFilename+' to '+destinationFilename)
@@ -525,7 +585,11 @@ def inboxAfterCapabilities(session,keyId: str,handle: str,messageJson: {}, \
     copyfile(queueFilename,destinationFilename)
     return True
 
-def runInboxQueue(baseDir: str,httpPrefix: str,sendThreads: [],postLog: [],cachedWebfingers: {},personCache: {},queue: [],domain: str,port: int,useTor: bool,federationList: [],ocapAlways: bool,debug: bool,acceptedCaps=["inbox:write","objects:read"]) -> None:
+def runInboxQueue(baseDir: str,httpPrefix: str,sendThreads: [],postLog: [], \
+                  cachedWebfingers: {},personCache: {},queue: [], \
+                  domain: str,port: int,useTor: bool,federationList: [], \
+                  ocapAlways: bool,debug: bool, \
+                  acceptedCaps=["inbox:write","objects:read"]) -> None:
     """Processes received items and moves them to
     the appropriate directories
     """