diff --git a/activitypub.py b/activitypub.py index 904231a..9df6c82 100644 --- a/activitypub.py +++ b/activitypub.py @@ -194,61 +194,6 @@ class ActivityPub(Protocol): else: error('HTTP Signature verification failed', status=401) - @classmethod - def accept_follow(cls, obj, user): - """Replies to an AP Follow request with an Accept request. - - TODO: move to Protocol - - Args: - obj: :class:`Object` - user: :class:`User` - """ - logger.info('Replying to Follow with Accept') - - followee = obj.as2.get('object') - followee_id = followee.get('id') if isinstance(followee, dict) else followee - follower = obj.as2.get('actor') - if not followee or not followee_id or not follower: - error(f'Follow activity requires object and actor. Got: {follow}') - - inbox = follower.get('inbox') - follower_id = follower.get('id') - if not inbox or not follower_id: - error(f'Follow actor requires id and inbox. Got: {follower}') - - # rendered mf2 HTML proxy pages (in render.py) fall back to redirecting to - # the follow's AS2 id field, but Mastodon's ids are URLs that don't load in - # browsers, eg https://jawns.club/ac33c547-ca6b-4351-80d5-d11a6879a7b0 - # so, set a synthetic URL based on the follower's profile. - # https://github.com/snarfed/bridgy-fed/issues/336 - follower_url = util.get_url(follower) or follower_id - followee_url = util.get_url(followee) or followee_id - obj.as2.setdefault('url', f'{follower_url}#followed-{followee_url}') - - # store Follower - follower_obj = Follower.get_or_create( - dest=user.key.id(), src=follower_id, last_follow=obj.as2) - follower_obj.status = 'active' - follower_obj.put() - - # send AP Accept - followee_actor_url = common.host_url(user.key.id()) - accept = { - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': util.tag_uri(common.PRIMARY_DOMAIN, - f'accept/{user.key.id()}/{obj.key.id()}'), - 'type': 'Accept', - 'actor': followee_actor_url, - 'object': { - 'type': 'Follow', - 'actor': follower_id, - 'object': followee_actor_url, - } - } - - return cls.send(inbox, accept, user=user) - def signed_get(url, *, user=None, **kwargs): return signed_request(util.requests_get, url, user=user, **kwargs) @@ -545,8 +490,9 @@ def inbox(domain=None): body = request.get_data(as_text=True) error(f"Couldn't parse body as non-empty JSON mapping: {body}", exc_info=True) + type = activity.get('type') actor_id = as1.get_object(activity, 'actor').get('id') - logger.info(f'Got {activity.get("type")} activity from {actor_id}: {json_dumps(activity, indent=2)}') + logger.info(f'Got {type} from {actor_id}: {json_dumps(activity, indent=2)}') # load user # TODO: store in g instead of passing around @@ -562,10 +508,22 @@ def inbox(domain=None): # follows, or other activity types, since Mastodon doesn't currently mark # those as explicitly public. Use as2's is_public instead of as1's because # as1's interprets unlisted as true. - if activity.get('type') == 'Create' and not as2.is_public(activity): + if type == 'Create' and not as2.is_public(activity): logger.info('Dropping non-public activity') return 'OK' + if type == 'Follow': + # rendered mf2 HTML proxy pages (in render.py) fall back to redirecting + # to the follow's AS2 id field, but Mastodon's Accept ids are URLs that + # don't load in browsers, eg: + # https://jawns.club/ac33c547-ca6b-4351-80d5-d11a6879a7b0 + # + # so, set a synthetic URL based on the follower's profile. + # https://github.com/snarfed/bridgy-fed/issues/336 + follower_url = redirect_unwrap(util.get_url(activity, 'actor')) + followee_url = redirect_unwrap(util.get_url(activity, 'object')) + activity.setdefault('url', f'{follower_url}#followed-{followee_url}') + return ActivityPub.receive(activity.get('id'), user=user, as2=redirect_unwrap(activity)) diff --git a/protocol.py b/protocol.py index cf69c78..9989921 100644 --- a/protocol.py +++ b/protocol.py @@ -12,6 +12,7 @@ from common import error # import module instead of individual classes to avoid circular import import models from oauth_dropins.webutil import util, webmention +from oauth_dropins.webutil.util import json_dumps, json_loads SUPPORTED_TYPES = ( 'accept', @@ -114,6 +115,8 @@ class Protocol: obj.populate(source_protocol=cls.LABEL, **props) obj.put() + logging.info(f'Got AS1: {json_dumps(obj.as1, indent=2)}') + if obj.type not in SUPPORTED_TYPES: error(f'Sorry, {obj.type} activities are not supported yet.', status=501) @@ -193,7 +196,7 @@ class Protocol: inner_obj = obj.as2['object'] = cls.get_object(inner_obj_id, user=user).as2 if obj.type == 'follow': - resp = cls.accept_follow(obj, user) + cls.accept_follow(obj, user=user) # send webmentions to each target send_webmentions(obj, proxy=True) @@ -215,6 +218,52 @@ class Protocol: obj.put() return 'OK' + @classmethod + def accept_follow(cls, obj, *, user=None): + """Replies to an AP Follow request with an Accept request. + + TODO: move to Protocol + + Args: + obj: :class:`Object` + user: :class:`User` + """ + logger.info('Replying to Follow with Accept') + + followee = as1.get_object(obj.as1) + followee_id = followee.get('id') + follower = as1.get_object(obj.as1, 'actor') + if not followee or not followee_id or not follower: + error(f'Follow activity requires object and actor. Got: {follow}') + + inbox = follower.get('inbox') + follower_id = follower.get('id') + if not inbox or not follower_id: + error(f'Follow actor requires id and inbox. Got: {follower}') + + # store Follower + follower_obj = models.Follower.get_or_create( + dest=user.key.id(), src=follower_id, last_follow=obj.as2) + follower_obj.status = 'active' + follower_obj.put() + + # send AP Accept + followee_actor_url = common.host_url(user.key.id()) + accept = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': util.tag_uri(common.PRIMARY_DOMAIN, + f'accept/{user.key.id()}/{obj.key.id()}'), + 'type': 'Accept', + 'actor': followee_actor_url, + 'object': { + 'type': 'Follow', + 'actor': follower_id, + 'object': followee_actor_url, + } + } + + return cls.send(inbox, accept, user=user) + @classmethod @cached(LRUCache(1000), key=lambda cls, id, user=None: util.fragmentless(id), lock=threading.Lock())