Verified Commit 6aa6f1d8 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Test actor can now follow back

parent 2f6d3ae1
......@@ -83,3 +83,37 @@ def deliver(activity, on_behalf_of, to=[]):
)
response.raise_for_status()
logger.debug('Remote answered with %s', response.status_code)
def get_follow(follow_id, follower, followed):
return {
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
{}
],
'actor': follower.url,
'id': follower.url + '#follows/{}'.format(follow_id),
'object': followed.url,
'type': 'Follow'
}
def get_accept_follow(accept_id, accept_actor, follow, follow_actor):
return {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{}
],
"id": accept_actor.url + '#accepts/follows/{}'.format(
accept_id),
"type": "Accept",
"actor": accept_actor.url,
"object": {
"id": follow['id'],
"type": "Follow",
"actor": follow_actor.url,
"object": accept_actor.url
},
}
import logging
import requests
import uuid
import xml
from django.conf import settings
......@@ -98,7 +99,7 @@ class SystemActor(object):
raise NotImplementedError
def post_inbox(self, data, actor=None):
raise NotImplementedError
return self.handle(data, actor=actor)
def get_outbox(self, data, actor=None):
raise NotImplementedError
......@@ -106,6 +107,31 @@ class SystemActor(object):
def post_outbox(self, data, actor=None):
raise NotImplementedError
def handle(self, data, actor=None):
"""
Main entrypoint for handling activities posted to the
actor's inbox
"""
logger.info('Received activity on %s inbox', self.id)
if actor is None:
raise PermissionDenied('Actor not authenticated')
serializer = serializers.ActivitySerializer(
data=data, context={'actor': actor})
serializer.is_valid(raise_exception=True)
ac = serializer.data
try:
handler = getattr(
self, 'handle_{}'.format(ac['type'].lower()))
except (KeyError, AttributeError):
logger.debug(
'No handler for activity %s', ac['type'])
return
return handler(ac, actor)
class LibraryActor(SystemActor):
id = 'library'
......@@ -147,23 +173,6 @@ class TestActor(SystemActor):
"orderedItems": []
}
def post_inbox(self, data, actor=None):
if actor is None:
raise PermissionDenied('Actor not authenticated')
serializer = serializers.ActivitySerializer(
data=data, context={'actor': actor})
serializer.is_valid(raise_exception=True)
ac = serializer.validated_data
logger.info('Received activity on %s inbox', self.id)
if ac['type'] == 'Create' and ac['object']['type'] == 'Note':
# we received a toot \o/
command = self.parse_command(ac['object']['content'])
logger.debug('Parsed command: %s', command)
if command == 'ping':
self.handle_ping(ac, actor)
def parse_command(self, message):
"""
Remove any links or fancy markup to extract /command from
......@@ -175,7 +184,16 @@ class TestActor(SystemActor):
except IndexError:
return
def handle_ping(self, ac, sender):
def handle_create(self, ac, sender):
if ac['object']['type'] != 'Note':
return
# we received a toot \o/
command = self.parse_command(ac['object']['content'])
logger.debug('Parsed command: %s', command)
if command != 'ping':
return
now = timezone.now()
test_actor = self.get_actor_instance()
reply_url = 'https://{}/activities/note/{}'.format(
......@@ -221,6 +239,31 @@ class TestActor(SystemActor):
to=[ac['actor']],
on_behalf_of=test_actor)
def handle_follow(self, ac, sender):
# on a follow we:
# 1. send the accept answer
# 2. follow back
test_actor = self.get_actor_instance()
accept_uuid = uuid.uuid4()
accept = activity.get_accept_follow(
accept_id=accept_uuid,
accept_actor=test_actor,
follow=ac,
follow_actor=sender)
activity.deliver(
accept,
to=[ac['actor']],
on_behalf_of=test_actor)
follow_uuid = uuid.uuid4()
follow = activity.get_follow(
follow_id=follow_uuid,
follower=test_actor,
followed=sender)
activity.deliver(
follow,
to=[ac['actor']],
on_behalf_of=test_actor)
SYSTEM_ACTORS = {
'library': LibraryActor(),
'test': TestActor(),
......
......@@ -89,3 +89,20 @@ class NoteFactory(factory.Factory):
class Meta:
model = dict
@registry.register(name='federation.Activity')
class ActivityFactory(factory.Factory):
type = 'Create'
id = factory.Faker('url')
published = factory.LazyFunction(
lambda: timezone.now().isoformat()
)
actor = factory.Faker('url')
object = factory.SubFactory(
NoteFactory,
actor=factory.SelfAttribute('..actor'),
published=factory.SelfAttribute('..published'))
class Meta:
model = dict
......@@ -120,7 +120,9 @@ class ActivitySerializer(serializers.Serializer):
type = value['type']
except KeyError:
raise serializers.ValidationError('Missing object type')
except TypeError:
# probably a URL
return value
try:
object_serializer = OBJECT_SERIALIZERS[type]
except KeyError:
......
import pytest
import uuid
from django.urls import reverse
from django.utils import timezone
......@@ -127,7 +128,7 @@ def test_test_post_outbox_validates_actor(nodb_factories):
assert msg in exc_info.value
def test_test_post_outbox_handles_create_note(
def test_test_post_inbox_handles_create_note(
settings, mocker, factories):
deliver = mocker.patch(
'funkwhale_api.federation.activity.deliver')
......@@ -238,3 +239,77 @@ def test_library_actor_manually_approves_based_on_setting(
settings.FEDERATION_MUSIC_NEEDS_APPROVAL = value
library_conf = actors.SYSTEM_ACTORS['library']
assert library_conf.manually_approves_followers is value
def test_system_actor_handle(mocker, nodb_factories):
handler = mocker.patch(
'funkwhale_api.federation.actors.TestActor.handle_create')
actor = nodb_factories['federation.Actor']()
activity = nodb_factories['federation.Activity'](
type='Create', actor=actor.url)
serializer = serializers.ActivitySerializer(
data=activity
)
assert serializer.is_valid()
actors.SYSTEM_ACTORS['test'].handle(activity, actor)
handler.assert_called_once_with(serializer.data, actor)
def test_test_actor_handles_follow(
settings, mocker, factories):
deliver = mocker.patch(
'funkwhale_api.federation.activity.deliver')
actor = factories['federation.Actor']()
now = timezone.now()
mocker.patch('django.utils.timezone.now', return_value=now)
test_actor = actors.SYSTEM_ACTORS['test'].get_actor_instance()
data = {
'actor': actor.url,
'type': 'Follow',
'id': 'http://test.federation/user#follows/267',
'object': test_actor.url,
}
uid = uuid.uuid4()
mocker.patch('uuid.uuid4', return_value=uid)
expected_accept = {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{}
],
"id": test_actor.url + '#accepts/follows/{}'.format(uid),
"type": "Accept",
"actor": test_actor.url,
"object": {
"id": data['id'],
"type": "Follow",
"actor": actor.url,
"object": test_actor.url
},
}
expected_follow = {
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
{}
],
'actor': test_actor.url,
'id': test_actor.url + '#follows/{}'.format(uid),
'object': actor.url,
'type': 'Follow'
}
actors.SYSTEM_ACTORS['test'].post_inbox(data, actor=actor)
expected_calls = [
mocker.call(
expected_accept,
to=[actor.url],
on_behalf_of=test_actor,
),
mocker.call(
expected_follow,
to=[actor.url],
on_behalf_of=test_actor,
)
]
deliver.assert_has_calls(expected_calls)
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment