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 ...@@ -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=[]): ...@@ -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): ...@@ -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)
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( serializer.save()
system_actor, ac, sender
)
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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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))
......
# 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): ...@@ -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): ...@@ -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): ...@@ -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())
......
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 ...@@ -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.m