diff --git a/activitypub.py b/activitypub.py index f1de0a8..156a560 100644 --- a/activitypub.py +++ b/activitypub.py @@ -1,3 +1,4 @@ +import logging import json import binascii import os @@ -21,6 +22,8 @@ import tasks from typing import List, Optional, Dict, Any, Union from typing import TypeVar +logger = logging.getLogger(__name__) + A = TypeVar('A', bound='BaseActivity') ObjectType = Dict[str, Any] ObjectOrIDType = Union[str, ObjectType] @@ -454,6 +457,9 @@ class Follow(BaseActivity): def _undo_inbox(self) -> None: DB.followers.delete_one({'remote_actor': self.get_actor().id}) + def _undo_outbox(self) -> None: + DB.following.delete_one({'remote_actor': self.get_object().id}) + def build_accept(self) -> BaseActivity: return self._build_reply(ActivityTypes.ACCEPT) @@ -517,12 +523,17 @@ class Undo(BaseActivity): return False def _post_to_outbox(self, obj_id: str, activity: ObjectType, recipients: List[str]) -> None: + logger.debug('processing undo to outbox') + logger.debug('self={}'.format(self)) obj = self.get_object() + logger.debug('obj={}'.format(obj)) DB.outbox.update_one({'remote_id': obj.id}, {'$set': {'meta.undo': True}}) try: obj._undo_outbox() + logger.debug(f'_undo_outbox called for {obj}') except NotImplementedError: + logger.debug(f'_undo_outbox not implemented for {obj}') pass diff --git a/app.py b/app.py index 0b19ef0..7eaf668 100644 --- a/app.py +++ b/app.py @@ -615,16 +615,25 @@ def ui_like(): like.post_to_outbox() return redirect(request.args.get('redirect')) -@app.route('/ui/undo') -@login_required -def ui_undo(): +@app.route('/api/undo', methods=['GET', 'POST']) +@api_required +def api_undo(): oid = request.args.get('id') - doc =DB.outbox.find_one({'id': oid}) + doc = DB.outbox.find_one({'$or': [{'id': oid}, {'remote_id': oid}]}) + undo_id = None if doc: obj = activitypub.parse_activity(doc.get('activity')) + # FIXME(tsileo): detect already undo-ed and make this API call idempotent undo = obj.build_undo() undo.post_to_outbox() - return redirect(request.args.get('redirect')) + undo_id = undo.id + if request.args.get('redirect'): + return redirect(request.args.get('redirect')) + return Response( + status=201, + headers={'Microblogpub-Created-Activity': undo_id}, + ) + @app.route('/stream') @login_required @@ -823,6 +832,7 @@ def api_follow(): follow.post_to_outbox() return Response( status=201, + headers={'Microblogpub-Created-Activity': follow.id}, ) diff --git a/tests/federation_test.py b/tests/federation_test.py index dc3d17d..ff8db17 100644 --- a/tests/federation_test.py +++ b/tests/federation_test.py @@ -58,6 +58,15 @@ class Instance(object): # We need to wait for the Follow/Accept dance time.sleep(10) + 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 + + # We need to wait for the Follow/Accept dance + time.sleep(10) + return resp.headers.get('microblogpub-created-activity') def followers(self): resp = self.session.get(f'{self.host_url}/followers', headers={'Accept': 'application/activity+json'}) @@ -81,8 +90,7 @@ class Instance(object): return resp.json() -def test_federation(): - """Ensure the homepage is accessible.""" +def _instances(): instance1 = Instance('http://localhost:5006', 'http://instance1_web_1:5005') instance1.ping() @@ -94,16 +102,54 @@ def test_federation(): instance1.drop_db() instance2.login() instance2.drop_db() + + return instance1, instance2 + +def test_follow(): + instance1, instance2 = _instances() # Instance1 follows instance2 instance1.follow(instance2) instance1_debug = instance1.debug() + print(f'instance1_debug={instance1_debug}') + assert instance1_debug['inbox'] == 1 # An Accept activity should be there + assert instance1_debug['outbox'] == 1 # We've sent a Follow activity + + instance2_debug = instance2.debug() + print(f'instance2_debug={instance2_debug}') + assert instance2_debug['inbox'] == 1 # An Follow activity should be there + assert instance2_debug['outbox'] == 1 # We've sent a Accept activity + + assert instance2.followers() == [instance1.docker_url] + assert instance1.following() == [instance2.docker_url] + + +def test_follow_unfollow(): + instance1, instance2 = _instances() + # Instance1 follows instance2 + follow_id = instance1.follow(instance2) + instance1_debug = instance1.debug() assert instance1_debug['inbox'] == 1 # An Accept activity should be there assert instance1_debug['outbox'] == 1 # We've sent a Follow activity instance2_debug = instance2.debug() - assert instance1_debug['inbox'] == 1 # An Follow activity should be there - assert instance1_debug['outbox'] == 1 # We've sent a Accept activity + assert instance2_debug['inbox'] == 1 # An Follow activity should be there + assert instance2_debug['outbox'] == 1 # We've sent a Accept activity assert instance2.followers() == [instance1.docker_url] assert instance1.following() == [instance2.docker_url] + + instance1.undo(follow_id) + + assert instance2.followers() == [] + assert instance1.following() == [] + + instance1_debug = instance1.debug() + assert instance1_debug['inbox'] == 1 # An Accept activity should be there + assert instance1_debug['outbox'] == 2 # We've sent a Follow and a Undo activity + + instance2_debug = instance2.debug() + assert instance2_debug['inbox'] == 2 # An Follow and Undo activity should be there + assert instance2_debug['outbox'] == 1 # We've sent a Accept activity + +