Verified Commit 0b2fe843 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Removed too complex FollowRequest model, we now use an aproved field on Follow

parent c97db31c
......@@ -6,8 +6,10 @@ import uuid
from django.conf import settings
from funkwhale_api.common import session
from funkwhale_api.common import utils as funkwhale_utils
from . import models
from . import serializers
from . import signing
logger = logging.getLogger(__name__)
......@@ -85,66 +87,9 @@ def deliver(activity, on_behalf_of, to=[]):
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_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)
def accept_follow(follow):
serializer = serializers.AcceptFollowSerializer(follow)
deliver(
accept,
to=[actor.url],
on_behalf_of=target)
return models.Follow.objects.get_or_create(
actor=actor,
target=target,
)
serializer.data,
to=[follow.actor.url],
on_behalf_of=follow.target)
......@@ -153,24 +153,32 @@ class SystemActor(object):
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
serializer = serializers.FollowSerializer(
data=ac, context={'follow_actor': sender})
if not serializer.is_valid():
return logger.info('Invalid follow payload')
approved = True if not self.manually_approves_followers else None
follow = serializer.save(approved=approved)
if follow.approved:
return activity.accept_follow(follow)
def handle_accept(self, 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')
return activity.accept_follow(
system_actor, ac, sender
)
serializer.save()
def handle_undo_follow(self, ac, sender):
actor = self.get_actor_instance()
models.Follow.objects.filter(
actor=sender,
target=actor,
).delete()
system_actor = self.get_actor_instance()
serializer = serializers.UndoFollowSerializer(
data=ac, context={'actor': sender, 'target': system_actor})
if not serializer.is_valid():
return logger.info('Received invalid payload')
serializer.save()
def handle_undo(self, ac, sender):
if ac['object']['type'] != 'Follow':
......@@ -206,20 +214,6 @@ class LibraryActor(SystemActor):
def manually_approves_followers(self):
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
def handle_create(self, ac, sender):
try:
......@@ -360,15 +354,15 @@ class TestActor(SystemActor):
super().handle_follow(ac, sender)
# also, we follow back
test_actor = self.get_actor_instance()
follow_uuid = uuid.uuid4()
follow = activity.get_follow(
follow_id=follow_uuid,
follower=test_actor,
followed=sender)
follow_back = models.Follow.objects.get_or_create(
actor=test_actor,
target=sender,
approved=None,
)[0]
activity.deliver(
follow,
to=[ac['actor']],
on_behalf_of=test_actor)
serializers.FollowSerializer(follow_back).data,
to=[follow_back.target.url],
on_behalf_of=follow_back.actor)
def handle_undo_follow(self, ac, sender):
super().handle_undo_follow(ac, sender)
......@@ -381,11 +375,7 @@ class TestActor(SystemActor):
)
except models.Follow.DoesNotExist:
return
undo = activity.get_undo(
id=follow.get_federation_url(),
actor=actor,
object=serializers.FollowSerializer(follow).data,
)
undo = serializers.UndoFollowSerializer(follow).data
follow.delete()
activity.deliver(
undo,
......
......@@ -23,24 +23,13 @@ class FollowAdmin(admin.ModelAdmin):
list_display = [
'actor',
'target',
'approved',
'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 = [
'approved'
]
search_fields = ['actor__url', 'target__url']
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
class LibraryFactory(factory.DjangoModelFactory):
actor = factory.SubFactory(ActorFactory)
......
......@@ -38,25 +38,21 @@ def scan_from_account_name(account_name):
actor__domain=domain,
actor__preferred_username=username
).select_related('actor').first()
follow_request = None
if library:
data['local']['following'] = True
data['local']['awaiting_approval'] = True
else:
follow_request = models.FollowRequest.objects.filter(
data['local'] = {
'following': False,
'awaiting_approval': False,
}
try:
follow = models.Follow.objects.get(
target__preferred_username=username,
target__domain=username,
actor=system_library,
).first()
data['local'] = {
'following': False,
'awaiting_approval': False,
}
if follow_request:
data['awaiting_approval'] = follow_request.approved is None
)
data['local']['awaiting_approval'] = not bool(follow.approved)
data['local']['following'] = True
except models.Follow.DoesNotExist:
pass
follow_request = models.Follow
try:
data['webfinger'] = webfinger.get_resource(
'acct:{}'.format(account_name))
......
# 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)
modification_date = models.DateTimeField(
auto_now=True)
approved = models.NullBooleanField(default=None)
class Meta:
unique_together = ['actor', 'target']
......@@ -117,49 +118,6 @@ class Follow(models.Model):
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):
creation_date = models.DateTimeField(default=timezone.now)
modification_date = models.DateTimeField(
......
......@@ -121,28 +121,132 @@ class LibraryActorSerializer(ActorSerializer):
return validated_data
class FollowSerializer(serializers.ModelSerializer):
# left maps to activitypub fields, right to our internal models
id = serializers.URLField(source='get_federation_url')
object = serializers.URLField(source='target.url')
actor = serializers.URLField(source='actor.url')
type = serializers.CharField(source='ap_type')
class FollowSerializer(serializers.Serializer):
id = serializers.URLField()
object = serializers.URLField()
actor = serializers.URLField()
type = serializers.ChoiceField(choices=['Follow'])
class Meta:
model = models.Actor
fields = [
'id',
'object',
'actor',
'type'
]
def validate_object(self, v):
expected = self.context.get('follow_target')
if expected and expected.url != v:
raise serializers.ValidationError('Invalid target')
try:
return models.Actor.objects.get(url=v)
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):
ret = super().to_representation(instance)
ret['@context'] = AP_CONTEXT
return {
'@context': AP_CONTEXT,
'actor': instance.actor.url,
'id': instance.get_federation_url(),
'object': instance.target.url,
'type': 'Follow'
}
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):
subject = serializers.CharField()
aliases = serializers.ListField(child=serializers.URLField())
......
from django import forms
from django.conf import settings
from django.core import paginator
from django.db import transaction
from django.http import HttpResponse
from django.urls import reverse
......@@ -9,9 +10,12 @@ from rest_framework import response
from rest_framework import views
from rest_framework import viewsets
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 . import activity
from . import actors
from . import authentication
from . import library
......@@ -172,3 +176,29 @@ class LibraryViewSet(viewsets.GenericViewSet):
data = library.scan_from_account_name(account)
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)
import uuid
from funkwhale_api.federation import activity
from funkwhale_api.federation import serializers
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):
deliver = mocker.patch(
'funkwhale_api.federation.activity.deliver')
actor = factories['federation.Actor']()
target = factories['federation.Actor'](local=True)
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":