Skip to content
Snippets Groups Projects
Verified Commit 6aa6f1d8 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Test actor can now follow back

parent 2f6d3ae1
No related branches found
No related tags found
No related merge requests found
......@@ -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)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment