Test actor can now follow back
This commit is contained in:
parent
2f6d3ae180
commit
6aa6f1d8f8
|
@ -83,3 +83,37 @@ def deliver(activity, on_behalf_of, to=[]):
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
logger.debug('Remote answered with %s', response.status_code)
|
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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
|
import uuid
|
||||||
import xml
|
import xml
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -98,7 +99,7 @@ class SystemActor(object):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def post_inbox(self, data, actor=None):
|
def post_inbox(self, data, actor=None):
|
||||||
raise NotImplementedError
|
return self.handle(data, actor=actor)
|
||||||
|
|
||||||
def get_outbox(self, data, actor=None):
|
def get_outbox(self, data, actor=None):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
@ -106,6 +107,31 @@ class SystemActor(object):
|
||||||
def post_outbox(self, data, actor=None):
|
def post_outbox(self, data, actor=None):
|
||||||
raise NotImplementedError
|
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):
|
class LibraryActor(SystemActor):
|
||||||
id = 'library'
|
id = 'library'
|
||||||
|
@ -147,23 +173,6 @@ class TestActor(SystemActor):
|
||||||
"orderedItems": []
|
"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):
|
def parse_command(self, message):
|
||||||
"""
|
"""
|
||||||
Remove any links or fancy markup to extract /command from
|
Remove any links or fancy markup to extract /command from
|
||||||
|
@ -175,7 +184,16 @@ class TestActor(SystemActor):
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return
|
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()
|
now = timezone.now()
|
||||||
test_actor = self.get_actor_instance()
|
test_actor = self.get_actor_instance()
|
||||||
reply_url = 'https://{}/activities/note/{}'.format(
|
reply_url = 'https://{}/activities/note/{}'.format(
|
||||||
|
@ -221,6 +239,31 @@ class TestActor(SystemActor):
|
||||||
to=[ac['actor']],
|
to=[ac['actor']],
|
||||||
on_behalf_of=test_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 = {
|
SYSTEM_ACTORS = {
|
||||||
'library': LibraryActor(),
|
'library': LibraryActor(),
|
||||||
'test': TestActor(),
|
'test': TestActor(),
|
||||||
|
|
|
@ -89,3 +89,20 @@ class NoteFactory(factory.Factory):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = dict
|
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']
|
type = value['type']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise serializers.ValidationError('Missing object type')
|
raise serializers.ValidationError('Missing object type')
|
||||||
|
except TypeError:
|
||||||
|
# probably a URL
|
||||||
|
return value
|
||||||
try:
|
try:
|
||||||
object_serializer = OBJECT_SERIALIZERS[type]
|
object_serializer = OBJECT_SERIALIZERS[type]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
import uuid
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -127,7 +128,7 @@ def test_test_post_outbox_validates_actor(nodb_factories):
|
||||||
assert msg in exc_info.value
|
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):
|
settings, mocker, factories):
|
||||||
deliver = mocker.patch(
|
deliver = mocker.patch(
|
||||||
'funkwhale_api.federation.activity.deliver')
|
'funkwhale_api.federation.activity.deliver')
|
||||||
|
@ -238,3 +239,77 @@ def test_library_actor_manually_approves_based_on_setting(
|
||||||
settings.FEDERATION_MUSIC_NEEDS_APPROVAL = value
|
settings.FEDERATION_MUSIC_NEEDS_APPROVAL = value
|
||||||
library_conf = actors.SYSTEM_ACTORS['library']
|
library_conf = actors.SYSTEM_ACTORS['library']
|
||||||
assert library_conf.manually_approves_followers is value
|
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)
|
||||||
|
|
Loading…
Reference in New Issue