diff --git a/activitypub.py b/activitypub.py index 252feca..7610a69 100644 --- a/activitypub.py +++ b/activitypub.py @@ -119,7 +119,7 @@ class BaseActivity(object): actor = self._validate_person(actor) self._data['actor'] = actor else: - if not self.NO_CONTEXT: + if not self.NO_CONTEXT and self.ACTIVITY_TYPE != ActivityType.TOMBSTONE: actor = ID self._data['actor'] = actor @@ -299,6 +299,7 @@ class BaseActivity(object): self.verify() actor = self.get_actor() + # Check for Block activity if DB.outbox.find_one({'type': ActivityType.BLOCK.value, 'activity.object': actor.id, 'meta.undo': False}): @@ -415,6 +416,9 @@ class BaseActivity(object): def build_undo(self) -> 'BaseActivity': raise NotImplementedError + def build_delete(self) -> 'BaseActivity': + raise NotImplementedError + class Person(BaseActivity): ACTIVITY_TYPE = ActivityType.PERSON @@ -645,6 +649,7 @@ class Announce(BaseActivity): '$inc': {'meta.count_boost': 1}, '$addToSet': {'meta.col_shares': self.to_dict(embed=True, embed_object_id_only=True)}, }) + def _undo_inbox(self) -> None: obj = self.get_object() # Update the meta counter if the object is published by the server @@ -683,14 +688,17 @@ class Delete(BaseActivity): ALLOWED_OBJECT_TYPES = [ActivityType.NOTE, ActivityType.TOMBSTONE] def _recipients(self) -> List[str]: - return self.get_object().recipients() + obj = self.get_object() + if obj.type_enum == ActivityType.TOMBSTONE: + obj = parse_activity(OBJECT_SERVICE.get(obj.id)) + return obj._recipients() - def _process_from_inbox(self): + def _process_from_inbox(self) -> None: DB.inbox.update_one({'activity.object.id': self.get_object().id}, {'$set': {'meta.deleted': True}}) # TODO(tsileo): also delete copies stored in parents' `meta.replies` # TODO(tsileo): also purge the cache if it's a reply of a published activity - def _post_to_outbox(self, obj_id, activity, recipients): + def _post_to_outbox(self, obj_id: str, activity: ObjectType, recipients: List[str]) -> None: DB.outbox.update_one({'activity.object.id': self.get_object().id}, {'$set': {'meta.deleted': True}}) @@ -870,6 +878,9 @@ class Note(BaseActivity): published=datetime.utcnow().replace(microsecond=0).isoformat() + 'Z', ) + def build_delete(self) -> BaseActivity: + return Delete(object=Tombstone(id=self.id).to_dict(embed=True)) + _ACTIVITY_TYPE_TO_CLS = { ActivityType.IMAGE: Image, @@ -946,7 +957,7 @@ def build_inbox_json_feed(path: str, request_cursor: Optional[str] = None) -> Di data = [] cursor = None - q: Dict[str, Any] = {'type': 'Create'} + q: Dict[str, Any] = {'type': 'Create', 'meta.deleted': False} if request_cursor: q['_id'] = {'$lt': request_cursor} diff --git a/app.py b/app.py index a5f47e2..b28715b 100644 --- a/app.py +++ b/app.py @@ -622,9 +622,25 @@ def notifications(): ) +@app.route('/api/delete') +@api_required +def api_delete(): + # FIXME(tsileo): ensure a Note and not a Create is given + oid = request.args.get('id') + obj = activitypub.parse_activity(OBJECT_SERVICE.get(oid)) + delete = obj.build_delete() + delete.post_to_outbox() + if request.args.get('redirect'): + return redirect(request.args.get('redirect')) + return Response( + status=201, + headers={'Microblogpub-Created-Activity': delete.id}, + ) + @app.route('/api/boost') @api_required def api_boost(): + # FIXME(tsileo): ensure a Note and not a Create is given oid = request.args.get('id') obj = activitypub.parse_activity(OBJECT_SERVICE.get(oid)) announce = obj.build_announce() diff --git a/tests/federation_test.py b/tests/federation_test.py index 0b3c2e9..9da231d 100644 --- a/tests/federation_test.py +++ b/tests/federation_test.py @@ -82,6 +82,13 @@ class Instance(object): time.sleep(self._create_delay) return resp.headers.get('microblogpub-created-activity') + def delete(self, oid: str) -> None: + resp = self.session.get(f'{self.host_url}/api/delete', params={'id': oid}) + assert resp.status_code == 201 + + time.sleep(self._create_delay) + return resp.headers.get('microblogpub-created-activity') + def undo(self, oid: str) -> None: resp = self.session.get(f'{self.host_url}/api/undo', params={'id': oid}) assert resp.status_code == 201 @@ -203,6 +210,35 @@ def test_post_content(): assert inbox_stream['items'][0]['id'] == create_id +def test_post_content_and_delete(): + instance1, instance2 = _instances() + # Instance1 follows instance2 + instance1.follow(instance2) + instance2.follow(instance1) + + inbox_stream = instance2.stream_jsonfeed() + assert len(inbox_stream['items']) == 0 + + create_id = instance1.new_note('hello') + instance2_debug = instance2.debug() + assert instance2_debug['inbox'] == 3 # An Follow, Accept and Create activity should be there + instance2_debug['outbox'] == 2 # We've sent a Accept and a Follow activity + + # Ensure the post is visible in instance2's stream + inbox_stream = instance2.stream_jsonfeed() + assert len(inbox_stream['items']) == 1 + assert inbox_stream['items'][0]['id'] == create_id + + instance1.delete(f'{create_id}/activity') + instance2_debug = instance2.debug() + assert instance2_debug['inbox'] == 4 # An Follow, Accept and Create and Delete activity should be there + instance2_debug['outbox'] == 2 # We've sent a Accept and a Follow activity + + # Ensure the post has been delete from instance2's stream + inbox_stream = instance2.stream_jsonfeed() + assert len(inbox_stream['items']) == 0 + + def test_post_content_and_like(): instance1, instance2 = _instances() # Instance1 follows instance2