Removed too complex FollowRequest model, we now use an aproved field on Follow
This commit is contained in:
parent
c97db31cb1
commit
0b2fe8439a
|
@ -6,8 +6,10 @@ import uuid
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from funkwhale_api.common import session
|
from funkwhale_api.common import session
|
||||||
|
from funkwhale_api.common import utils as funkwhale_utils
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
from . import serializers
|
||||||
from . import signing
|
from . import signing
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -85,66 +87,9 @@ def deliver(activity, on_behalf_of, to=[]):
|
||||||
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):
|
def accept_follow(follow):
|
||||||
return {
|
serializer = serializers.AcceptFollowSerializer(follow)
|
||||||
'@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_undo(id, actor, object):
|
|
||||||
return {
|
|
||||||
'@context': [
|
|
||||||
'https://www.w3.org/ns/activitystreams',
|
|
||||||
'https://w3id.org/security/v1',
|
|
||||||
{}
|
|
||||||
],
|
|
||||||
'type': 'Undo',
|
|
||||||
'id': id + '/undo',
|
|
||||||
'actor': actor.url,
|
|
||||||
'object': object,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def accept_follow(target, follow, actor):
|
|
||||||
accept_uuid = uuid.uuid4()
|
|
||||||
accept = get_accept_follow(
|
|
||||||
accept_id=accept_uuid,
|
|
||||||
accept_actor=target,
|
|
||||||
follow=follow,
|
|
||||||
follow_actor=actor)
|
|
||||||
deliver(
|
deliver(
|
||||||
accept,
|
serializer.data,
|
||||||
to=[actor.url],
|
to=[follow.actor.url],
|
||||||
on_behalf_of=target)
|
on_behalf_of=follow.target)
|
||||||
return models.Follow.objects.get_or_create(
|
|
||||||
actor=actor,
|
|
||||||
target=target,
|
|
||||||
)
|
|
||||||
|
|
|
@ -153,24 +153,32 @@ class SystemActor(object):
|
||||||
|
|
||||||
def handle_follow(self, ac, sender):
|
def handle_follow(self, ac, sender):
|
||||||
system_actor = self.get_actor_instance()
|
system_actor = self.get_actor_instance()
|
||||||
if self.manually_approves_followers:
|
serializer = serializers.FollowSerializer(
|
||||||
fr, created = models.FollowRequest.objects.get_or_create(
|
data=ac, context={'follow_actor': sender})
|
||||||
actor=sender,
|
if not serializer.is_valid():
|
||||||
target=system_actor,
|
return logger.info('Invalid follow payload')
|
||||||
approved=None,
|
approved = True if not self.manually_approves_followers else None
|
||||||
)
|
follow = serializer.save(approved=approved)
|
||||||
return fr
|
if follow.approved:
|
||||||
|
return activity.accept_follow(follow)
|
||||||
|
|
||||||
return activity.accept_follow(
|
def handle_accept(self, ac, sender):
|
||||||
system_actor, ac, sender
|
system_actor = self.get_actor_instance()
|
||||||
)
|
serializer = serializers.AcceptFollowSerializer(
|
||||||
|
data=ac,
|
||||||
|
context={'follow_target': sender, 'follow_actor': system_actor})
|
||||||
|
if not serializer.is_valid(raise_exception=True):
|
||||||
|
return logger.info('Received invalid payload')
|
||||||
|
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
def handle_undo_follow(self, ac, sender):
|
def handle_undo_follow(self, ac, sender):
|
||||||
actor = self.get_actor_instance()
|
system_actor = self.get_actor_instance()
|
||||||
models.Follow.objects.filter(
|
serializer = serializers.UndoFollowSerializer(
|
||||||
actor=sender,
|
data=ac, context={'actor': sender, 'target': system_actor})
|
||||||
target=actor,
|
if not serializer.is_valid():
|
||||||
).delete()
|
return logger.info('Received invalid payload')
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
def handle_undo(self, ac, sender):
|
def handle_undo(self, ac, sender):
|
||||||
if ac['object']['type'] != 'Follow':
|
if ac['object']['type'] != 'Follow':
|
||||||
|
@ -206,20 +214,6 @@ class LibraryActor(SystemActor):
|
||||||
def manually_approves_followers(self):
|
def manually_approves_followers(self):
|
||||||
return settings.FEDERATION_MUSIC_NEEDS_APPROVAL
|
return settings.FEDERATION_MUSIC_NEEDS_APPROVAL
|
||||||
|
|
||||||
def handle_follow(self, ac, sender):
|
|
||||||
system_actor = self.get_actor_instance()
|
|
||||||
if self.manually_approves_followers:
|
|
||||||
fr, created = models.FollowRequest.objects.get_or_create(
|
|
||||||
actor=sender,
|
|
||||||
target=system_actor,
|
|
||||||
approved=None,
|
|
||||||
)
|
|
||||||
return fr
|
|
||||||
|
|
||||||
return activity.accept_follow(
|
|
||||||
system_actor, ac, sender
|
|
||||||
)
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def handle_create(self, ac, sender):
|
def handle_create(self, ac, sender):
|
||||||
try:
|
try:
|
||||||
|
@ -360,15 +354,15 @@ class TestActor(SystemActor):
|
||||||
super().handle_follow(ac, sender)
|
super().handle_follow(ac, sender)
|
||||||
# also, we follow back
|
# also, we follow back
|
||||||
test_actor = self.get_actor_instance()
|
test_actor = self.get_actor_instance()
|
||||||
follow_uuid = uuid.uuid4()
|
follow_back = models.Follow.objects.get_or_create(
|
||||||
follow = activity.get_follow(
|
actor=test_actor,
|
||||||
follow_id=follow_uuid,
|
target=sender,
|
||||||
follower=test_actor,
|
approved=None,
|
||||||
followed=sender)
|
)[0]
|
||||||
activity.deliver(
|
activity.deliver(
|
||||||
follow,
|
serializers.FollowSerializer(follow_back).data,
|
||||||
to=[ac['actor']],
|
to=[follow_back.target.url],
|
||||||
on_behalf_of=test_actor)
|
on_behalf_of=follow_back.actor)
|
||||||
|
|
||||||
def handle_undo_follow(self, ac, sender):
|
def handle_undo_follow(self, ac, sender):
|
||||||
super().handle_undo_follow(ac, sender)
|
super().handle_undo_follow(ac, sender)
|
||||||
|
@ -381,11 +375,7 @@ class TestActor(SystemActor):
|
||||||
)
|
)
|
||||||
except models.Follow.DoesNotExist:
|
except models.Follow.DoesNotExist:
|
||||||
return
|
return
|
||||||
undo = activity.get_undo(
|
undo = serializers.UndoFollowSerializer(follow).data
|
||||||
id=follow.get_federation_url(),
|
|
||||||
actor=actor,
|
|
||||||
object=serializers.FollowSerializer(follow).data,
|
|
||||||
)
|
|
||||||
follow.delete()
|
follow.delete()
|
||||||
activity.deliver(
|
activity.deliver(
|
||||||
undo,
|
undo,
|
||||||
|
|
|
@ -23,24 +23,13 @@ class FollowAdmin(admin.ModelAdmin):
|
||||||
list_display = [
|
list_display = [
|
||||||
'actor',
|
'actor',
|
||||||
'target',
|
'target',
|
||||||
|
'approved',
|
||||||
'creation_date'
|
'creation_date'
|
||||||
]
|
]
|
||||||
search_fields = ['actor__url', 'target__url']
|
|
||||||
list_select_related = True
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.FollowRequest)
|
|
||||||
class FollowRequestAdmin(admin.ModelAdmin):
|
|
||||||
list_display = [
|
|
||||||
'actor',
|
|
||||||
'target',
|
|
||||||
'creation_date',
|
|
||||||
'approved'
|
|
||||||
]
|
|
||||||
search_fields = ['actor__url', 'target__url']
|
|
||||||
list_filter = [
|
list_filter = [
|
||||||
'approved'
|
'approved'
|
||||||
]
|
]
|
||||||
|
search_fields = ['actor__url', 'target__url']
|
||||||
list_select_related = True
|
list_select_related = True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -113,15 +113,6 @@ class FollowFactory(factory.DjangoModelFactory):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@registry.register
|
|
||||||
class FollowRequestFactory(factory.DjangoModelFactory):
|
|
||||||
target = factory.SubFactory(ActorFactory)
|
|
||||||
actor = factory.SubFactory(ActorFactory)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.FollowRequest
|
|
||||||
|
|
||||||
|
|
||||||
@registry.register
|
@registry.register
|
||||||
class LibraryFactory(factory.DjangoModelFactory):
|
class LibraryFactory(factory.DjangoModelFactory):
|
||||||
actor = factory.SubFactory(ActorFactory)
|
actor = factory.SubFactory(ActorFactory)
|
||||||
|
|
|
@ -38,25 +38,21 @@ def scan_from_account_name(account_name):
|
||||||
actor__domain=domain,
|
actor__domain=domain,
|
||||||
actor__preferred_username=username
|
actor__preferred_username=username
|
||||||
).select_related('actor').first()
|
).select_related('actor').first()
|
||||||
follow_request = None
|
data['local'] = {
|
||||||
if library:
|
'following': False,
|
||||||
data['local']['following'] = True
|
'awaiting_approval': False,
|
||||||
data['local']['awaiting_approval'] = True
|
}
|
||||||
|
try:
|
||||||
else:
|
follow = models.Follow.objects.get(
|
||||||
follow_request = models.FollowRequest.objects.filter(
|
|
||||||
target__preferred_username=username,
|
target__preferred_username=username,
|
||||||
target__domain=username,
|
target__domain=username,
|
||||||
actor=system_library,
|
actor=system_library,
|
||||||
).first()
|
)
|
||||||
data['local'] = {
|
data['local']['awaiting_approval'] = not bool(follow.approved)
|
||||||
'following': False,
|
data['local']['following'] = True
|
||||||
'awaiting_approval': False,
|
except models.Follow.DoesNotExist:
|
||||||
}
|
pass
|
||||||
if follow_request:
|
|
||||||
data['awaiting_approval'] = follow_request.approved is None
|
|
||||||
|
|
||||||
follow_request = models.Follow
|
|
||||||
try:
|
try:
|
||||||
data['webfinger'] = webfinger.get_resource(
|
data['webfinger'] = webfinger.get_resource(
|
||||||
'acct:{}'.format(account_name))
|
'acct:{}'.format(account_name))
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Generated by Django 2.0.3 on 2018-04-10 16:24
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('federation', '0003_auto_20180407_1010'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='followrequest',
|
||||||
|
name='actor',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='followrequest',
|
||||||
|
name='target',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='follow',
|
||||||
|
name='approved',
|
||||||
|
field=models.NullBooleanField(default=None),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='FollowRequest',
|
||||||
|
),
|
||||||
|
]
|
|
@ -109,6 +109,7 @@ class Follow(models.Model):
|
||||||
creation_date = models.DateTimeField(default=timezone.now)
|
creation_date = models.DateTimeField(default=timezone.now)
|
||||||
modification_date = models.DateTimeField(
|
modification_date = models.DateTimeField(
|
||||||
auto_now=True)
|
auto_now=True)
|
||||||
|
approved = models.NullBooleanField(default=None)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ['actor', 'target']
|
unique_together = ['actor', 'target']
|
||||||
|
@ -117,49 +118,6 @@ class Follow(models.Model):
|
||||||
return '{}#follows/{}'.format(self.actor.url, self.uuid)
|
return '{}#follows/{}'.format(self.actor.url, self.uuid)
|
||||||
|
|
||||||
|
|
||||||
class FollowRequest(models.Model):
|
|
||||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
|
|
||||||
actor = models.ForeignKey(
|
|
||||||
Actor,
|
|
||||||
related_name='emmited_follow_requests',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
)
|
|
||||||
target = models.ForeignKey(
|
|
||||||
Actor,
|
|
||||||
related_name='received_follow_requests',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
)
|
|
||||||
creation_date = models.DateTimeField(default=timezone.now)
|
|
||||||
modification_date = models.DateTimeField(
|
|
||||||
auto_now=True)
|
|
||||||
approved = models.NullBooleanField(default=None)
|
|
||||||
|
|
||||||
def approve(self):
|
|
||||||
from . import activity
|
|
||||||
from . import serializers
|
|
||||||
self.approved = True
|
|
||||||
self.save(update_fields=['approved'])
|
|
||||||
Follow.objects.get_or_create(
|
|
||||||
target=self.target,
|
|
||||||
actor=self.actor
|
|
||||||
)
|
|
||||||
if self.target.is_local:
|
|
||||||
follow = {
|
|
||||||
'@context': serializers.AP_CONTEXT,
|
|
||||||
'actor': self.actor.url,
|
|
||||||
'id': self.actor.url + '#follows/{}'.format(uuid.uuid4()),
|
|
||||||
'object': self.target.url,
|
|
||||||
'type': 'Follow'
|
|
||||||
}
|
|
||||||
activity.accept_follow(
|
|
||||||
self.target, follow, self.actor
|
|
||||||
)
|
|
||||||
|
|
||||||
def refuse(self):
|
|
||||||
self.approved = False
|
|
||||||
self.save(update_fields=['approved'])
|
|
||||||
|
|
||||||
|
|
||||||
class Library(models.Model):
|
class Library(models.Model):
|
||||||
creation_date = models.DateTimeField(default=timezone.now)
|
creation_date = models.DateTimeField(default=timezone.now)
|
||||||
modification_date = models.DateTimeField(
|
modification_date = models.DateTimeField(
|
||||||
|
|
|
@ -121,28 +121,132 @@ class LibraryActorSerializer(ActorSerializer):
|
||||||
return validated_data
|
return validated_data
|
||||||
|
|
||||||
|
|
||||||
class FollowSerializer(serializers.ModelSerializer):
|
class FollowSerializer(serializers.Serializer):
|
||||||
# left maps to activitypub fields, right to our internal models
|
id = serializers.URLField()
|
||||||
id = serializers.URLField(source='get_federation_url')
|
object = serializers.URLField()
|
||||||
object = serializers.URLField(source='target.url')
|
actor = serializers.URLField()
|
||||||
actor = serializers.URLField(source='actor.url')
|
type = serializers.ChoiceField(choices=['Follow'])
|
||||||
type = serializers.CharField(source='ap_type')
|
|
||||||
|
|
||||||
class Meta:
|
def validate_object(self, v):
|
||||||
model = models.Actor
|
expected = self.context.get('follow_target')
|
||||||
fields = [
|
if expected and expected.url != v:
|
||||||
'id',
|
raise serializers.ValidationError('Invalid target')
|
||||||
'object',
|
try:
|
||||||
'actor',
|
return models.Actor.objects.get(url=v)
|
||||||
'type'
|
except models.Actor.DoesNotExist:
|
||||||
]
|
raise serializers.ValidationError('Target not found')
|
||||||
|
|
||||||
|
def validate_actor(self, v):
|
||||||
|
expected = self.context.get('follow_actor')
|
||||||
|
if expected and expected.url != v:
|
||||||
|
raise serializers.ValidationError('Invalid actor')
|
||||||
|
try:
|
||||||
|
return models.Actor.objects.get(url=v)
|
||||||
|
except models.Actor.DoesNotExist:
|
||||||
|
raise serializers.ValidationError('Actor not found')
|
||||||
|
|
||||||
|
def save(self, **kwargs):
|
||||||
|
return models.Follow.objects.get_or_create(
|
||||||
|
actor=self.validated_data['actor'],
|
||||||
|
target=self.validated_data['object'],
|
||||||
|
**kwargs,
|
||||||
|
)[0]
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
ret = super().to_representation(instance)
|
return {
|
||||||
ret['@context'] = AP_CONTEXT
|
'@context': AP_CONTEXT,
|
||||||
|
'actor': instance.actor.url,
|
||||||
|
'id': instance.get_federation_url(),
|
||||||
|
'object': instance.target.url,
|
||||||
|
'type': 'Follow'
|
||||||
|
}
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class AcceptFollowSerializer(serializers.Serializer):
|
||||||
|
id = serializers.URLField()
|
||||||
|
actor = serializers.URLField()
|
||||||
|
object = FollowSerializer()
|
||||||
|
type = serializers.ChoiceField(choices=['Accept'])
|
||||||
|
|
||||||
|
def validate_actor(self, v):
|
||||||
|
expected = self.context.get('follow_target')
|
||||||
|
if expected and expected.url != v:
|
||||||
|
raise serializers.ValidationError('Invalid actor')
|
||||||
|
try:
|
||||||
|
return models.Actor.objects.get(url=v)
|
||||||
|
except models.Actor.DoesNotExist:
|
||||||
|
raise serializers.ValidationError('Actor not found')
|
||||||
|
|
||||||
|
def validate(self, validated_data):
|
||||||
|
# we ensure the accept actor actually match the follow target
|
||||||
|
if validated_data['actor'] != validated_data['object']['object']:
|
||||||
|
raise serializers.ValidationError('Actor mismatch')
|
||||||
|
try:
|
||||||
|
validated_data['follow'] = models.Follow.objects.filter(
|
||||||
|
target=validated_data['actor'],
|
||||||
|
actor=validated_data['object']['actor']
|
||||||
|
).exclude(approved=True).get()
|
||||||
|
except models.Follow.DoesNotExist:
|
||||||
|
raise serializers.ValidationError('No follow to accept')
|
||||||
|
return validated_data
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
return {
|
||||||
|
"@context": AP_CONTEXT,
|
||||||
|
"id": instance.get_federation_url() + '/accept',
|
||||||
|
"type": "Accept",
|
||||||
|
"actor": instance.target.url,
|
||||||
|
"object": FollowSerializer(instance).data
|
||||||
|
}
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
self.validated_data['follow'].approved = True
|
||||||
|
self.validated_data['follow'].save()
|
||||||
|
return self.validated_data['follow']
|
||||||
|
|
||||||
|
|
||||||
|
class UndoFollowSerializer(serializers.Serializer):
|
||||||
|
id = serializers.URLField()
|
||||||
|
actor = serializers.URLField()
|
||||||
|
object = FollowSerializer()
|
||||||
|
type = serializers.ChoiceField(choices=['Undo'])
|
||||||
|
|
||||||
|
def validate_actor(self, v):
|
||||||
|
expected = self.context.get('follow_target')
|
||||||
|
if expected and expected.url != v:
|
||||||
|
raise serializers.ValidationError('Invalid actor')
|
||||||
|
try:
|
||||||
|
return models.Actor.objects.get(url=v)
|
||||||
|
except models.Actor.DoesNotExist:
|
||||||
|
raise serializers.ValidationError('Actor not found')
|
||||||
|
|
||||||
|
def validate(self, validated_data):
|
||||||
|
# we ensure the accept actor actually match the follow actor
|
||||||
|
if validated_data['actor'] != validated_data['object']['actor']:
|
||||||
|
raise serializers.ValidationError('Actor mismatch')
|
||||||
|
try:
|
||||||
|
validated_data['follow'] = models.Follow.objects.filter(
|
||||||
|
actor=validated_data['actor'],
|
||||||
|
target=validated_data['object']['object']
|
||||||
|
).get()
|
||||||
|
except models.Follow.DoesNotExist:
|
||||||
|
raise serializers.ValidationError('No follow to remove')
|
||||||
|
return validated_data
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
return {
|
||||||
|
"@context": AP_CONTEXT,
|
||||||
|
"id": instance.get_federation_url() + '/undo',
|
||||||
|
"type": "Undo",
|
||||||
|
"actor": instance.actor.url,
|
||||||
|
"object": FollowSerializer(instance).data
|
||||||
|
}
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
self.validated_data['follow'].delete()
|
||||||
|
|
||||||
|
|
||||||
class ActorWebfingerSerializer(serializers.Serializer):
|
class ActorWebfingerSerializer(serializers.Serializer):
|
||||||
subject = serializers.CharField()
|
subject = serializers.CharField()
|
||||||
aliases = serializers.ListField(child=serializers.URLField())
|
aliases = serializers.ListField(child=serializers.URLField())
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import paginator
|
from django.core import paginator
|
||||||
|
from django.db import transaction
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
@ -9,9 +10,12 @@ from rest_framework import response
|
||||||
from rest_framework import views
|
from rest_framework import views
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.decorators import list_route, detail_route
|
from rest_framework.decorators import list_route, detail_route
|
||||||
|
from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
|
from funkwhale_api.common import utils as funkwhale_utils
|
||||||
from funkwhale_api.music.models import TrackFile
|
from funkwhale_api.music.models import TrackFile
|
||||||
|
|
||||||
|
from . import activity
|
||||||
from . import actors
|
from . import actors
|
||||||
from . import authentication
|
from . import authentication
|
||||||
from . import library
|
from . import library
|
||||||
|
@ -172,3 +176,29 @@ class LibraryViewSet(viewsets.GenericViewSet):
|
||||||
|
|
||||||
data = library.scan_from_account_name(account)
|
data = library.scan_from_account_name(account)
|
||||||
return response.Response(data)
|
return response.Response(data)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
actor_url = request.data['actor_url']
|
||||||
|
except KeyError:
|
||||||
|
raise ValidationError('Missing actor_url')
|
||||||
|
|
||||||
|
try:
|
||||||
|
actor = actors.get_actor(actor_url)
|
||||||
|
library_data = library.get_library_data(actor.url)
|
||||||
|
except Exception as e:
|
||||||
|
raise ValidationError('Error while fetching actor and library')
|
||||||
|
|
||||||
|
library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
|
||||||
|
follow, created = models.Follow.objects.get_or_create(
|
||||||
|
actor=library_actor,
|
||||||
|
target=actor,
|
||||||
|
)
|
||||||
|
serializer = serializers.FollowSerializer(follow)
|
||||||
|
activity.deliver(
|
||||||
|
serializer.data,
|
||||||
|
on_behalf_of=library_actor,
|
||||||
|
to=[actor.url]
|
||||||
|
)
|
||||||
|
return response.Response({}, status=201)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from funkwhale_api.federation import activity
|
from funkwhale_api.federation import activity
|
||||||
|
from funkwhale_api.federation import serializers
|
||||||
|
|
||||||
|
|
||||||
def test_deliver(nodb_factories, r_mock, mocker):
|
def test_deliver(nodb_factories, r_mock, mocker):
|
||||||
|
@ -38,37 +39,9 @@ def test_deliver(nodb_factories, r_mock, mocker):
|
||||||
def test_accept_follow(mocker, factories):
|
def test_accept_follow(mocker, factories):
|
||||||
deliver = mocker.patch(
|
deliver = mocker.patch(
|
||||||
'funkwhale_api.federation.activity.deliver')
|
'funkwhale_api.federation.activity.deliver')
|
||||||
actor = factories['federation.Actor']()
|
follow = factories['federation.Follow'](approved=None)
|
||||||
target = factories['federation.Actor'](local=True)
|
expected_accept = serializers.AcceptFollowSerializer(follow).data
|
||||||
follow = {
|
activity.accept_follow(follow)
|
||||||
'actor': actor.url,
|
|
||||||
'type': 'Follow',
|
|
||||||
'id': 'http://test.federation/user#follows/267',
|
|
||||||
'object': target.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": target.url + '#accepts/follows/{}'.format(uid),
|
|
||||||
"type": "Accept",
|
|
||||||
"actor": target.url,
|
|
||||||
"object": {
|
|
||||||
"id": follow['id'],
|
|
||||||
"type": "Follow",
|
|
||||||
"actor": actor.url,
|
|
||||||
"object": target.url
|
|
||||||
},
|
|
||||||
}
|
|
||||||
activity.accept_follow(
|
|
||||||
target, follow, actor
|
|
||||||
)
|
|
||||||
deliver.assert_called_once_with(
|
deliver.assert_called_once_with(
|
||||||
expected_accept, to=[actor.url], on_behalf_of=target
|
expected_accept, to=[follow.actor.url], on_behalf_of=follow.target
|
||||||
)
|
)
|
||||||
follow_instance = actor.emitted_follows.first()
|
|
||||||
assert follow_instance.target == target
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django.utils import timezone
|
||||||
|
|
||||||
from rest_framework import exceptions
|
from rest_framework import exceptions
|
||||||
|
|
||||||
|
from funkwhale_api.federation import activity
|
||||||
from funkwhale_api.federation import actors
|
from funkwhale_api.federation import actors
|
||||||
from funkwhale_api.federation import models
|
from funkwhale_api.federation import models
|
||||||
from funkwhale_api.federation import serializers
|
from funkwhale_api.federation import serializers
|
||||||
|
@ -261,8 +262,6 @@ def test_test_actor_handles_follow(
|
||||||
deliver = mocker.patch(
|
deliver = mocker.patch(
|
||||||
'funkwhale_api.federation.activity.deliver')
|
'funkwhale_api.federation.activity.deliver')
|
||||||
actor = factories['federation.Actor']()
|
actor = factories['federation.Actor']()
|
||||||
now = timezone.now()
|
|
||||||
mocker.patch('django.utils.timezone.now', return_value=now)
|
|
||||||
accept_follow = mocker.patch(
|
accept_follow = mocker.patch(
|
||||||
'funkwhale_api.federation.activity.accept_follow')
|
'funkwhale_api.federation.activity.accept_follow')
|
||||||
test_actor = actors.SYSTEM_ACTORS['test'].get_actor_instance()
|
test_actor = actors.SYSTEM_ACTORS['test'].get_actor_instance()
|
||||||
|
@ -272,28 +271,15 @@ def test_test_actor_handles_follow(
|
||||||
'id': 'http://test.federation/user#follows/267',
|
'id': 'http://test.federation/user#follows/267',
|
||||||
'object': test_actor.url,
|
'object': test_actor.url,
|
||||||
}
|
}
|
||||||
uid = uuid.uuid4()
|
|
||||||
mocker.patch('uuid.uuid4', return_value=uid)
|
|
||||||
expected_follow = {
|
|
||||||
'@context': serializers.AP_CONTEXT,
|
|
||||||
'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)
|
actors.SYSTEM_ACTORS['test'].post_inbox(data, actor=actor)
|
||||||
accept_follow.assert_called_once_with(
|
follow = models.Follow.objects.get(target=test_actor, approved=True)
|
||||||
test_actor, data, actor
|
follow_back = models.Follow.objects.get(actor=test_actor, approved=None)
|
||||||
|
accept_follow.assert_called_once_with(follow)
|
||||||
|
deliver.assert_called_once_with(
|
||||||
|
serializers.FollowSerializer(follow_back).data,
|
||||||
|
on_behalf_of=test_actor,
|
||||||
|
to=[actor.url]
|
||||||
)
|
)
|
||||||
expected_calls = [
|
|
||||||
mocker.call(
|
|
||||||
expected_follow,
|
|
||||||
to=[actor.url],
|
|
||||||
on_behalf_of=test_actor,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
deliver.assert_has_calls(expected_calls)
|
|
||||||
|
|
||||||
|
|
||||||
def test_test_actor_handles_undo_follow(
|
def test_test_actor_handles_undo_follow(
|
||||||
|
@ -346,12 +332,10 @@ def test_library_actor_handles_follow_manual_approval(
|
||||||
}
|
}
|
||||||
|
|
||||||
library_actor.system_conf.post_inbox(data, actor=actor)
|
library_actor.system_conf.post_inbox(data, actor=actor)
|
||||||
fr = library_actor.received_follow_requests.first()
|
follow = library_actor.received_follows.first()
|
||||||
|
|
||||||
assert library_actor.received_follow_requests.count() == 1
|
assert follow.actor == actor
|
||||||
assert fr.target == library_actor
|
assert follow.approved is None
|
||||||
assert fr.actor == actor
|
|
||||||
assert fr.approved is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_library_actor_handles_follow_auto_approval(
|
def test_library_actor_handles_follow_auto_approval(
|
||||||
|
@ -369,10 +353,27 @@ def test_library_actor_handles_follow_auto_approval(
|
||||||
}
|
}
|
||||||
library_actor.system_conf.post_inbox(data, actor=actor)
|
library_actor.system_conf.post_inbox(data, actor=actor)
|
||||||
|
|
||||||
assert library_actor.received_follow_requests.count() == 0
|
follow = library_actor.received_follows.first()
|
||||||
accept_follow.assert_called_once_with(
|
|
||||||
library_actor, data, actor
|
assert follow.actor == actor
|
||||||
|
assert follow.approved is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_actor_handles_accept(
|
||||||
|
mocker, factories):
|
||||||
|
library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
|
||||||
|
actor = factories['federation.Actor']()
|
||||||
|
pending_follow = factories['federation.Follow'](
|
||||||
|
actor=library_actor,
|
||||||
|
target=actor,
|
||||||
|
approved=None,
|
||||||
)
|
)
|
||||||
|
serializer = serializers.AcceptFollowSerializer(pending_follow)
|
||||||
|
library_actor.system_conf.post_inbox(serializer.data, actor=actor)
|
||||||
|
|
||||||
|
pending_follow.refresh_from_db()
|
||||||
|
|
||||||
|
assert pending_follow.approved is True
|
||||||
|
|
||||||
|
|
||||||
def test_library_actor_handle_create_audio_no_library(mocker, factories):
|
def test_library_actor_handle_create_audio_no_library(mocker, factories):
|
||||||
|
|
|
@ -35,50 +35,6 @@ def test_follow_federation_url(factories):
|
||||||
assert follow.get_federation_url() == expected
|
assert follow.get_federation_url() == expected
|
||||||
|
|
||||||
|
|
||||||
def test_follow_request_approve(mocker, factories):
|
|
||||||
uid = uuid.uuid4()
|
|
||||||
mocker.patch('uuid.uuid4', return_value=uid)
|
|
||||||
accept_follow = mocker.patch(
|
|
||||||
'funkwhale_api.federation.activity.accept_follow')
|
|
||||||
fr = factories['federation.FollowRequest'](target__local=True)
|
|
||||||
fr.approve()
|
|
||||||
|
|
||||||
follow = {
|
|
||||||
'@context': serializers.AP_CONTEXT,
|
|
||||||
'actor': fr.actor.url,
|
|
||||||
'id': fr.actor.url + '#follows/{}'.format(uid),
|
|
||||||
'object': fr.target.url,
|
|
||||||
'type': 'Follow'
|
|
||||||
}
|
|
||||||
|
|
||||||
assert fr.approved is True
|
|
||||||
assert list(fr.target.followers.all()) == [fr.actor]
|
|
||||||
accept_follow.assert_called_once_with(
|
|
||||||
fr.target, follow, fr.actor
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_follow_request_approve_non_local(mocker, factories):
|
|
||||||
uid = uuid.uuid4()
|
|
||||||
mocker.patch('uuid.uuid4', return_value=uid)
|
|
||||||
accept_follow = mocker.patch(
|
|
||||||
'funkwhale_api.federation.activity.accept_follow')
|
|
||||||
fr = factories['federation.FollowRequest']()
|
|
||||||
fr.approve()
|
|
||||||
|
|
||||||
assert fr.approved is True
|
|
||||||
assert list(fr.target.followers.all()) == [fr.actor]
|
|
||||||
accept_follow.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
def test_follow_request_refused(mocker, factories):
|
|
||||||
fr = factories['federation.FollowRequest']()
|
|
||||||
fr.refuse()
|
|
||||||
|
|
||||||
assert fr.approved is False
|
|
||||||
assert fr.target.followers.count() == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_library_model_unique_per_actor(factories):
|
def test_library_model_unique_per_actor(factories):
|
||||||
library = factories['federation.Library']()
|
library = factories['federation.Library']()
|
||||||
with pytest.raises(db.IntegrityError):
|
with pytest.raises(db.IntegrityError):
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import arrow
|
import arrow
|
||||||
|
import pytest
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
|
@ -170,6 +171,184 @@ def test_follow_serializer_to_ap(factories):
|
||||||
assert serializer.data == expected
|
assert serializer.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_follow_serializer_save(factories):
|
||||||
|
actor = factories['federation.Actor']()
|
||||||
|
target = factories['federation.Actor']()
|
||||||
|
|
||||||
|
data = expected = {
|
||||||
|
'id': 'https://test.follow',
|
||||||
|
'type': 'Follow',
|
||||||
|
'actor': actor.url,
|
||||||
|
'object': target.url,
|
||||||
|
}
|
||||||
|
serializer = serializers.FollowSerializer(data=data)
|
||||||
|
|
||||||
|
assert serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
follow = serializer.save()
|
||||||
|
|
||||||
|
assert follow.pk is not None
|
||||||
|
assert follow.actor == actor
|
||||||
|
assert follow.target == target
|
||||||
|
assert follow.approved is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_follow_serializer_save_validates_on_context(factories):
|
||||||
|
actor = factories['federation.Actor']()
|
||||||
|
target = factories['federation.Actor']()
|
||||||
|
impostor = factories['federation.Actor']()
|
||||||
|
|
||||||
|
data = expected = {
|
||||||
|
'id': 'https://test.follow',
|
||||||
|
'type': 'Follow',
|
||||||
|
'actor': actor.url,
|
||||||
|
'object': target.url,
|
||||||
|
}
|
||||||
|
serializer = serializers.FollowSerializer(
|
||||||
|
data=data,
|
||||||
|
context={'follow_actor': impostor, 'follow_target': impostor})
|
||||||
|
|
||||||
|
assert serializer.is_valid() is False
|
||||||
|
|
||||||
|
assert 'actor' in serializer.errors
|
||||||
|
assert 'object' in serializer.errors
|
||||||
|
|
||||||
|
|
||||||
|
def test_accept_follow_serializer_representation(factories):
|
||||||
|
follow = factories['federation.Follow'](approved=None)
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
'https://w3id.org/security/v1',
|
||||||
|
{},
|
||||||
|
],
|
||||||
|
'id': follow.get_federation_url() + '/accept',
|
||||||
|
'type': 'Accept',
|
||||||
|
'actor': follow.target.url,
|
||||||
|
'object': serializers.FollowSerializer(follow).data,
|
||||||
|
}
|
||||||
|
|
||||||
|
serializer = serializers.AcceptFollowSerializer(follow)
|
||||||
|
|
||||||
|
assert serializer.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_accept_follow_serializer_save(factories):
|
||||||
|
follow = factories['federation.Follow'](approved=None)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
'https://w3id.org/security/v1',
|
||||||
|
{},
|
||||||
|
],
|
||||||
|
'id': follow.get_federation_url() + '/accept',
|
||||||
|
'type': 'Accept',
|
||||||
|
'actor': follow.target.url,
|
||||||
|
'object': serializers.FollowSerializer(follow).data,
|
||||||
|
}
|
||||||
|
|
||||||
|
serializer = serializers.AcceptFollowSerializer(data=data)
|
||||||
|
assert serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
follow.refresh_from_db()
|
||||||
|
|
||||||
|
assert follow.approved is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_accept_follow_serializer_validates_on_context(factories):
|
||||||
|
follow = factories['federation.Follow'](approved=None)
|
||||||
|
impostor = factories['federation.Actor']()
|
||||||
|
data = {
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
'https://w3id.org/security/v1',
|
||||||
|
{},
|
||||||
|
],
|
||||||
|
'id': follow.get_federation_url() + '/accept',
|
||||||
|
'type': 'Accept',
|
||||||
|
'actor': impostor.url,
|
||||||
|
'object': serializers.FollowSerializer(follow).data,
|
||||||
|
}
|
||||||
|
|
||||||
|
serializer = serializers.AcceptFollowSerializer(
|
||||||
|
data=data,
|
||||||
|
context={'follow_actor': impostor, 'follow_target': impostor})
|
||||||
|
|
||||||
|
assert serializer.is_valid() is False
|
||||||
|
assert 'actor' in serializer.errors['object']
|
||||||
|
assert 'object' in serializer.errors['object']
|
||||||
|
|
||||||
|
|
||||||
|
def test_undo_follow_serializer_representation(factories):
|
||||||
|
follow = factories['federation.Follow'](approved=True)
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
'https://w3id.org/security/v1',
|
||||||
|
{},
|
||||||
|
],
|
||||||
|
'id': follow.get_federation_url() + '/undo',
|
||||||
|
'type': 'Undo',
|
||||||
|
'actor': follow.actor.url,
|
||||||
|
'object': serializers.FollowSerializer(follow).data,
|
||||||
|
}
|
||||||
|
|
||||||
|
serializer = serializers.UndoFollowSerializer(follow)
|
||||||
|
|
||||||
|
assert serializer.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_undo_follow_serializer_save(factories):
|
||||||
|
follow = factories['federation.Follow'](approved=True)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
'https://w3id.org/security/v1',
|
||||||
|
{},
|
||||||
|
],
|
||||||
|
'id': follow.get_federation_url() + '/undo',
|
||||||
|
'type': 'Undo',
|
||||||
|
'actor': follow.actor.url,
|
||||||
|
'object': serializers.FollowSerializer(follow).data,
|
||||||
|
}
|
||||||
|
|
||||||
|
serializer = serializers.UndoFollowSerializer(data=data)
|
||||||
|
assert serializer.is_valid(raise_exception=True)
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
with pytest.raises(models.Follow.DoesNotExist):
|
||||||
|
follow.refresh_from_db()
|
||||||
|
|
||||||
|
|
||||||
|
def test_undo_follow_serializer_validates_on_context(factories):
|
||||||
|
follow = factories['federation.Follow'](approved=True)
|
||||||
|
impostor = factories['federation.Actor']()
|
||||||
|
data = {
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
'https://w3id.org/security/v1',
|
||||||
|
{},
|
||||||
|
],
|
||||||
|
'id': follow.get_federation_url() + '/undo',
|
||||||
|
'type': 'Undo',
|
||||||
|
'actor': impostor.url,
|
||||||
|
'object': serializers.FollowSerializer(follow).data,
|
||||||
|
}
|
||||||
|
|
||||||
|
serializer = serializers.UndoFollowSerializer(
|
||||||
|
data=data,
|
||||||
|
context={'follow_actor': impostor, 'follow_target': impostor})
|
||||||
|
|
||||||
|
assert serializer.is_valid() is False
|
||||||
|
assert 'actor' in serializer.errors['object']
|
||||||
|
assert 'object' in serializer.errors['object']
|
||||||
|
|
||||||
|
|
||||||
def test_paginated_collection_serializer(factories):
|
def test_paginated_collection_serializer(factories):
|
||||||
tfs = factories['music.TrackFile'].create_batch(size=5)
|
tfs = factories['music.TrackFile'].create_batch(size=5)
|
||||||
actor = factories['federation.Actor'](local=True)
|
actor = factories['federation.Actor'](local=True)
|
||||||
|
|
|
@ -4,6 +4,7 @@ from django.core.paginator import Paginator
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from funkwhale_api.federation import actors
|
from funkwhale_api.federation import actors
|
||||||
|
from funkwhale_api.federation import models
|
||||||
from funkwhale_api.federation import serializers
|
from funkwhale_api.federation import serializers
|
||||||
from funkwhale_api.federation import utils
|
from funkwhale_api.federation import utils
|
||||||
from funkwhale_api.federation import webfinger
|
from funkwhale_api.federation import webfinger
|
||||||
|
@ -179,3 +180,35 @@ def test_can_scan_library(superuser_api_client, mocker):
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.data == result
|
assert response.data == result
|
||||||
scan.assert_called_once_with('test@test.library')
|
scan.assert_called_once_with('test@test.library')
|
||||||
|
|
||||||
|
|
||||||
|
def test_follow_library_manually(superuser_api_client, mocker, factories):
|
||||||
|
library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
|
||||||
|
actor = factories['federation.Actor'](manually_approves_followers=True)
|
||||||
|
follow = {'test': 'follow'}
|
||||||
|
deliver = mocker.patch(
|
||||||
|
'funkwhale_api.federation.activity.deliver')
|
||||||
|
actor_get = mocker.patch(
|
||||||
|
'funkwhale_api.federation.actors.get_actor',
|
||||||
|
return_value=actor)
|
||||||
|
library_get = mocker.patch(
|
||||||
|
'funkwhale_api.federation.library.get_library_data',
|
||||||
|
return_value={})
|
||||||
|
|
||||||
|
url = reverse('api:v1:federation:libraries-list')
|
||||||
|
response = superuser_api_client.post(
|
||||||
|
url, {'actor_url': actor.url})
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
follow = models.Follow.objects.get(
|
||||||
|
actor=library_actor,
|
||||||
|
target=actor,
|
||||||
|
approved=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
deliver.assert_called_once_with(
|
||||||
|
serializers.FollowSerializer(follow).data,
|
||||||
|
on_behalf_of=library_actor,
|
||||||
|
to=[actor.url]
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue