kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
Merge branch 'cleanup-017' into 'develop'
Cleanup unused code See merge request funkwhale/funkwhale!426environments/review-docs-funkw-78jnxn/deployments/34
commit
42933fa138
|
@ -125,7 +125,6 @@ LOCAL_APPS = (
|
|||
"funkwhale_api.radios",
|
||||
"funkwhale_api.history",
|
||||
"funkwhale_api.playlists",
|
||||
"funkwhale_api.providers.audiofile",
|
||||
"funkwhale_api.providers.acoustid",
|
||||
"funkwhale_api.subsonic",
|
||||
)
|
||||
|
|
|
@ -1,26 +1,16 @@
|
|||
import datetime
|
||||
import logging
|
||||
import xml
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from funkwhale_api.common import preferences, session
|
||||
|
||||
from . import activity, keys, models, serializers, signing, utils
|
||||
from . import models, serializers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def remove_tags(text):
|
||||
logger.debug("Removing tags from %s", text)
|
||||
return "".join(
|
||||
xml.etree.ElementTree.fromstring("<div>{}</div>".format(text)).itertext()
|
||||
)
|
||||
|
||||
|
||||
def get_actor_data(actor_url):
|
||||
response = session.get_session().get(
|
||||
actor_url,
|
||||
|
@ -51,247 +41,3 @@ def get_actor(fid):
|
|||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
return serializer.save(last_fetch_date=timezone.now())
|
||||
|
||||
|
||||
class SystemActor(object):
|
||||
additional_attributes = {}
|
||||
manually_approves_followers = False
|
||||
|
||||
def get_request_auth(self):
|
||||
actor = self.get_actor_instance()
|
||||
return signing.get_auth(actor.private_key, actor.private_key_id)
|
||||
|
||||
def serialize(self):
|
||||
actor = self.get_actor_instance()
|
||||
serializer = serializers.ActorSerializer(actor)
|
||||
return serializer.data
|
||||
|
||||
def get_actor_instance(self):
|
||||
try:
|
||||
return models.Actor.objects.get(fid=self.get_actor_id())
|
||||
except models.Actor.DoesNotExist:
|
||||
pass
|
||||
private, public = keys.get_key_pair()
|
||||
args = self.get_instance_argument(
|
||||
self.id, name=self.name, summary=self.summary, **self.additional_attributes
|
||||
)
|
||||
args["private_key"] = private.decode("utf-8")
|
||||
args["public_key"] = public.decode("utf-8")
|
||||
return models.Actor.objects.create(**args)
|
||||
|
||||
def get_actor_id(self):
|
||||
return utils.full_url(
|
||||
reverse("federation:instance-actors-detail", kwargs={"actor": self.id})
|
||||
)
|
||||
|
||||
def get_instance_argument(self, id, name, summary, **kwargs):
|
||||
p = {
|
||||
"preferred_username": id,
|
||||
"domain": settings.FEDERATION_HOSTNAME,
|
||||
"type": "Person",
|
||||
"name": name.format(host=settings.FEDERATION_HOSTNAME),
|
||||
"manually_approves_followers": True,
|
||||
"fid": self.get_actor_id(),
|
||||
"shared_inbox_url": utils.full_url(
|
||||
reverse("federation:instance-actors-inbox", kwargs={"actor": id})
|
||||
),
|
||||
"inbox_url": utils.full_url(
|
||||
reverse("federation:instance-actors-inbox", kwargs={"actor": id})
|
||||
),
|
||||
"outbox_url": utils.full_url(
|
||||
reverse("federation:instance-actors-outbox", kwargs={"actor": id})
|
||||
),
|
||||
"summary": summary.format(host=settings.FEDERATION_HOSTNAME),
|
||||
}
|
||||
p.update(kwargs)
|
||||
return p
|
||||
|
||||
def get_inbox(self, data, actor=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def post_inbox(self, data, actor=None):
|
||||
return self.handle(data, actor=actor)
|
||||
|
||||
def get_outbox(self, data, actor=None):
|
||||
raise NotImplementedError
|
||||
|
||||
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(data, actor)
|
||||
|
||||
def handle_follow(self, ac, sender):
|
||||
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 serializer.save()
|
||||
|
||||
def handle_undo_follow(self, ac, sender):
|
||||
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":
|
||||
return
|
||||
|
||||
if ac["object"]["actor"] != sender.fid:
|
||||
# not the same actor, permission issue
|
||||
return
|
||||
|
||||
self.handle_undo_follow(ac, sender)
|
||||
|
||||
|
||||
class TestActor(SystemActor):
|
||||
id = "test"
|
||||
name = "{host}'s test account"
|
||||
summary = (
|
||||
"Bot account to test federation with {host}. "
|
||||
"Send me /ping and I'll answer you."
|
||||
)
|
||||
additional_attributes = {"manually_approves_followers": False}
|
||||
manually_approves_followers = False
|
||||
|
||||
def get_outbox(self, data, actor=None):
|
||||
return {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{},
|
||||
],
|
||||
"id": utils.full_url(
|
||||
reverse("federation:instance-actors-outbox", kwargs={"actor": self.id})
|
||||
),
|
||||
"type": "OrderedCollection",
|
||||
"totalItems": 0,
|
||||
"orderedItems": [],
|
||||
}
|
||||
|
||||
def parse_command(self, message):
|
||||
"""
|
||||
Remove any links or fancy markup to extract /command from
|
||||
a note message.
|
||||
"""
|
||||
raw = remove_tags(message)
|
||||
try:
|
||||
return raw.split("/")[1]
|
||||
except IndexError:
|
||||
return
|
||||
|
||||
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(
|
||||
settings.FEDERATION_HOSTNAME, now.timestamp()
|
||||
)
|
||||
reply_activity = {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{},
|
||||
],
|
||||
"type": "Create",
|
||||
"actor": test_actor.fid,
|
||||
"id": "{}/activity".format(reply_url),
|
||||
"published": now.isoformat(),
|
||||
"to": ac["actor"],
|
||||
"cc": [],
|
||||
"object": {
|
||||
"type": "Note",
|
||||
"content": "Pong!",
|
||||
"summary": None,
|
||||
"published": now.isoformat(),
|
||||
"id": reply_url,
|
||||
"inReplyTo": ac["object"]["id"],
|
||||
"sensitive": False,
|
||||
"url": reply_url,
|
||||
"to": [ac["actor"]],
|
||||
"attributedTo": test_actor.fid,
|
||||
"cc": [],
|
||||
"attachment": [],
|
||||
"tag": [
|
||||
{
|
||||
"type": "Mention",
|
||||
"href": ac["actor"],
|
||||
"name": sender.full_username,
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
activity.deliver(reply_activity, to=[ac["actor"]], on_behalf_of=test_actor)
|
||||
|
||||
def handle_follow(self, ac, sender):
|
||||
super().handle_follow(ac, sender)
|
||||
# also, we follow back
|
||||
test_actor = self.get_actor_instance()
|
||||
follow_back = models.Follow.objects.get_or_create(
|
||||
actor=test_actor, target=sender, approved=None
|
||||
)[0]
|
||||
activity.deliver(
|
||||
serializers.FollowSerializer(follow_back).data,
|
||||
to=[follow_back.target.fid],
|
||||
on_behalf_of=follow_back.actor,
|
||||
)
|
||||
|
||||
def handle_undo_follow(self, ac, sender):
|
||||
super().handle_undo_follow(ac, sender)
|
||||
actor = self.get_actor_instance()
|
||||
# we also unfollow the sender, if possible
|
||||
try:
|
||||
follow = models.Follow.objects.get(target=sender, actor=actor)
|
||||
except models.Follow.DoesNotExist:
|
||||
return
|
||||
undo = serializers.UndoFollowSerializer(follow).data
|
||||
follow.delete()
|
||||
activity.deliver(undo, to=[sender.fid], on_behalf_of=actor)
|
||||
|
||||
|
||||
SYSTEM_ACTORS = {"test": TestActor()}
|
||||
|
|
|
@ -1,74 +1,9 @@
|
|||
import json
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
|
||||
from funkwhale_api.common import session
|
||||
|
||||
from . import actors, models, serializers, signing, webfinger
|
||||
|
||||
|
||||
def scan_from_account_name(account_name):
|
||||
"""
|
||||
Given an account name such as library@test.library, will:
|
||||
|
||||
1. Perform the webfinger lookup
|
||||
2. Perform the actor lookup
|
||||
3. Perform the library's collection lookup
|
||||
|
||||
and return corresponding data in a dictionary.
|
||||
"""
|
||||
data = {}
|
||||
try:
|
||||
username, domain = webfinger.clean_acct(account_name, ensure_local=False)
|
||||
except serializers.ValidationError:
|
||||
return {"webfinger": {"errors": ["Invalid account string"]}}
|
||||
system_library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
|
||||
data["local"] = {"following": False, "awaiting_approval": False}
|
||||
try:
|
||||
follow = models.Follow.objects.get(
|
||||
target__preferred_username=username,
|
||||
target__domain=username,
|
||||
actor=system_library,
|
||||
)
|
||||
data["local"]["awaiting_approval"] = not bool(follow.approved)
|
||||
data["local"]["following"] = True
|
||||
except models.Follow.DoesNotExist:
|
||||
pass
|
||||
|
||||
try:
|
||||
data["webfinger"] = webfinger.get_resource("acct:{}".format(account_name))
|
||||
except requests.ConnectionError:
|
||||
return {"webfinger": {"errors": ["This webfinger resource is not reachable"]}}
|
||||
except requests.HTTPError as e:
|
||||
return {
|
||||
"webfinger": {
|
||||
"errors": [
|
||||
"Error {} during webfinger request".format(e.response.status_code)
|
||||
]
|
||||
}
|
||||
}
|
||||
except json.JSONDecodeError as e:
|
||||
return {"webfinger": {"errors": ["Could not process webfinger response"]}}
|
||||
|
||||
try:
|
||||
data["actor"] = actors.get_actor_data(data["webfinger"]["actor_url"])
|
||||
except requests.ConnectionError:
|
||||
data["actor"] = {"errors": ["This actor is not reachable"]}
|
||||
return data
|
||||
except requests.HTTPError as e:
|
||||
data["actor"] = {
|
||||
"errors": ["Error {} during actor request".format(e.response.status_code)]
|
||||
}
|
||||
return data
|
||||
|
||||
serializer = serializers.LibraryActorSerializer(data=data["actor"])
|
||||
if not serializer.is_valid():
|
||||
data["actor"] = {"errors": ["Invalid ActivityPub actor"]}
|
||||
return data
|
||||
data["library"] = get_library_data(serializer.validated_data["library_url"])
|
||||
|
||||
return data
|
||||
from . import serializers, signing
|
||||
|
||||
|
||||
def get_library_data(library_url, actor):
|
||||
|
|
|
@ -5,9 +5,7 @@ from . import views
|
|||
|
||||
router = routers.SimpleRouter(trailing_slash=False)
|
||||
music_router = routers.SimpleRouter(trailing_slash=False)
|
||||
router.register(
|
||||
r"federation/instance/actors", views.InstanceActorViewSet, "instance-actors"
|
||||
)
|
||||
|
||||
router.register(r"federation/shared", views.SharedViewSet, "shared")
|
||||
router.register(r"federation/actors", views.ActorViewSet, "actors")
|
||||
router.register(r".well-known", views.WellKnownViewSet, "well-known")
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django import forms
|
||||
from django.core import paginator
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse
|
||||
from rest_framework import exceptions, mixins, response, viewsets
|
||||
from rest_framework.decorators import detail_route, list_route
|
||||
|
@ -8,16 +8,7 @@ from rest_framework.decorators import detail_route, list_route
|
|||
from funkwhale_api.common import preferences
|
||||
from funkwhale_api.music import models as music_models
|
||||
|
||||
from . import (
|
||||
activity,
|
||||
actors,
|
||||
authentication,
|
||||
models,
|
||||
renderers,
|
||||
serializers,
|
||||
utils,
|
||||
webfinger,
|
||||
)
|
||||
from . import activity, authentication, models, renderers, serializers, utils, webfinger
|
||||
|
||||
|
||||
class FederationMixin(object):
|
||||
|
@ -78,47 +69,6 @@ class ActorViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericV
|
|||
return response.Response({})
|
||||
|
||||
|
||||
class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet):
|
||||
lookup_field = "actor"
|
||||
lookup_value_regex = "[a-z]*"
|
||||
authentication_classes = [authentication.SignatureAuthentication]
|
||||
permission_classes = []
|
||||
renderer_classes = [renderers.ActivityPubRenderer]
|
||||
|
||||
def get_object(self):
|
||||
try:
|
||||
return actors.SYSTEM_ACTORS[self.kwargs["actor"]]
|
||||
except KeyError:
|
||||
raise Http404
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
system_actor = self.get_object()
|
||||
actor = system_actor.get_actor_instance()
|
||||
data = actor.system_conf.serialize()
|
||||
return response.Response(data, status=200)
|
||||
|
||||
@detail_route(methods=["get", "post"])
|
||||
def inbox(self, request, *args, **kwargs):
|
||||
system_actor = self.get_object()
|
||||
handler = getattr(system_actor, "{}_inbox".format(request.method.lower()))
|
||||
|
||||
try:
|
||||
handler(request.data, actor=request.actor)
|
||||
except NotImplementedError:
|
||||
return response.Response(status=405)
|
||||
return response.Response({}, status=200)
|
||||
|
||||
@detail_route(methods=["get", "post"])
|
||||
def outbox(self, request, *args, **kwargs):
|
||||
system_actor = self.get_object()
|
||||
handler = getattr(system_actor, "{}_outbox".format(request.method.lower()))
|
||||
try:
|
||||
handler(request.data, actor=request.actor)
|
||||
except NotImplementedError:
|
||||
return response.Response(status=405)
|
||||
return response.Response({}, status=200)
|
||||
|
||||
|
||||
class WellKnownViewSet(viewsets.GenericViewSet):
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
|
@ -160,13 +110,10 @@ class WellKnownViewSet(viewsets.GenericViewSet):
|
|||
def handler_acct(self, clean_result):
|
||||
username, hostname = clean_result
|
||||
|
||||
if username in actors.SYSTEM_ACTORS:
|
||||
actor = actors.SYSTEM_ACTORS[username].get_actor_instance()
|
||||
else:
|
||||
try:
|
||||
actor = models.Actor.objects.local().get(preferred_username=username)
|
||||
except models.Actor.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid username")
|
||||
try:
|
||||
actor = models.Actor.objects.local().get(preferred_username=username)
|
||||
except models.Actor.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid username")
|
||||
|
||||
return serializers.ActorWebfingerSerializer(actor).data
|
||||
|
||||
|
|
|
@ -11,10 +11,8 @@ from musicbrainzngs import ResponseError
|
|||
from requests.exceptions import RequestException
|
||||
|
||||
from funkwhale_api.common import channels
|
||||
from funkwhale_api.common import preferences
|
||||
from funkwhale_api.federation import activity, actors, routes
|
||||
from funkwhale_api.federation import routes
|
||||
from funkwhale_api.federation import library as lb
|
||||
from funkwhale_api.federation import library as federation_serializers
|
||||
from funkwhale_api.taskapp import celery
|
||||
|
||||
from . import lyrics as lyrics_utils
|
||||
|
@ -78,45 +76,6 @@ def fetch_content(lyrics):
|
|||
lyrics.save(update_fields=["content"])
|
||||
|
||||
|
||||
@celery.app.task(name="music.import_batch_notify_followers")
|
||||
@celery.require_instance(
|
||||
models.ImportBatch.objects.filter(status="finished"), "import_batch"
|
||||
)
|
||||
def import_batch_notify_followers(import_batch):
|
||||
if not preferences.get("federation__enabled"):
|
||||
return
|
||||
|
||||
if import_batch.source == "federation":
|
||||
return
|
||||
|
||||
library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
|
||||
followers = library_actor.get_approved_followers()
|
||||
jobs = import_batch.jobs.filter(
|
||||
status="finished", library_track__isnull=True, upload__isnull=False
|
||||
).select_related("upload__track__artist", "upload__track__album__artist")
|
||||
uploads = [job.upload for job in jobs]
|
||||
collection = federation_serializers.CollectionSerializer(
|
||||
{
|
||||
"actor": library_actor,
|
||||
"id": import_batch.get_federation_id(),
|
||||
"items": uploads,
|
||||
"item_serializer": federation_serializers.AudioSerializer,
|
||||
}
|
||||
).data
|
||||
for f in followers:
|
||||
create = federation_serializers.ActivitySerializer(
|
||||
{
|
||||
"type": "Create",
|
||||
"id": collection["id"],
|
||||
"object": collection,
|
||||
"actor": library_actor.fid,
|
||||
"to": [f.url],
|
||||
}
|
||||
).data
|
||||
|
||||
activity.deliver(create, on_behalf_of=library_actor, to=[f.url])
|
||||
|
||||
|
||||
@celery.app.task(name="music.start_library_scan")
|
||||
@celery.require_instance(
|
||||
models.LibraryScan.objects.select_related().filter(status="pending"), "library_scan"
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
"""
|
||||
This module is responsible from importing existing audiofiles from the
|
||||
filesystem into funkwhale.
|
||||
"""
|
|
@ -1,8 +1,4 @@
|
|||
import pytest
|
||||
from django.urls import reverse
|
||||
from rest_framework import exceptions
|
||||
|
||||
from funkwhale_api.federation import actors, models, serializers, utils
|
||||
from funkwhale_api.federation import actors, serializers
|
||||
|
||||
|
||||
def test_actor_fetching(r_mock):
|
||||
|
@ -50,120 +46,3 @@ def test_get_actor_refresh(factories, preferences, mocker):
|
|||
assert new_actor == actor
|
||||
assert new_actor.last_fetch_date > actor.last_fetch_date
|
||||
assert new_actor.preferred_username == "New me"
|
||||
|
||||
|
||||
def test_get_test(db, mocker, settings):
|
||||
mocker.patch(
|
||||
"funkwhale_api.federation.keys.get_key_pair",
|
||||
return_value=(b"private", b"public"),
|
||||
)
|
||||
expected = {
|
||||
"preferred_username": "test",
|
||||
"domain": settings.FEDERATION_HOSTNAME,
|
||||
"type": "Person",
|
||||
"name": "{}'s test account".format(settings.FEDERATION_HOSTNAME),
|
||||
"manually_approves_followers": False,
|
||||
"public_key": "public",
|
||||
"fid": utils.full_url(
|
||||
reverse("federation:instance-actors-detail", kwargs={"actor": "test"})
|
||||
),
|
||||
"shared_inbox_url": utils.full_url(
|
||||
reverse("federation:instance-actors-inbox", kwargs={"actor": "test"})
|
||||
),
|
||||
"inbox_url": utils.full_url(
|
||||
reverse("federation:instance-actors-inbox", kwargs={"actor": "test"})
|
||||
),
|
||||
"outbox_url": utils.full_url(
|
||||
reverse("federation:instance-actors-outbox", kwargs={"actor": "test"})
|
||||
),
|
||||
"summary": "Bot account to test federation with {}. Send me /ping and I'll answer you.".format(
|
||||
settings.FEDERATION_HOSTNAME
|
||||
),
|
||||
}
|
||||
actor = actors.SYSTEM_ACTORS["test"].get_actor_instance()
|
||||
for key, value in expected.items():
|
||||
assert getattr(actor, key) == value
|
||||
|
||||
|
||||
def test_test_get_outbox():
|
||||
expected = {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{},
|
||||
],
|
||||
"id": utils.full_url(
|
||||
reverse("federation:instance-actors-outbox", kwargs={"actor": "test"})
|
||||
),
|
||||
"type": "OrderedCollection",
|
||||
"totalItems": 0,
|
||||
"orderedItems": [],
|
||||
}
|
||||
|
||||
data = actors.SYSTEM_ACTORS["test"].get_outbox({}, actor=None)
|
||||
|
||||
assert data == expected
|
||||
|
||||
|
||||
def test_test_post_inbox_requires_authenticated_actor():
|
||||
with pytest.raises(exceptions.PermissionDenied):
|
||||
actors.SYSTEM_ACTORS["test"].post_inbox({}, actor=None)
|
||||
|
||||
|
||||
def test_test_post_outbox_validates_actor(nodb_factories):
|
||||
actor = nodb_factories["federation.Actor"]()
|
||||
data = {"actor": "noop"}
|
||||
with pytest.raises(exceptions.ValidationError) as exc_info:
|
||||
actors.SYSTEM_ACTORS["test"].post_inbox(data, actor=actor)
|
||||
msg = "The actor making the request do not match"
|
||||
assert msg in exc_info.value
|
||||
|
||||
|
||||
def test_getting_actor_instance_persists_in_db(db):
|
||||
test = actors.SYSTEM_ACTORS["test"].get_actor_instance()
|
||||
from_db = models.Actor.objects.get(fid=test.fid)
|
||||
|
||||
for f in test._meta.fields:
|
||||
assert getattr(from_db, f.name) == getattr(test, f.name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"username,domain,expected",
|
||||
[("test", "wrongdomain.com", False), ("notsystem", "", False), ("test", "", True)],
|
||||
)
|
||||
def test_actor_is_system(username, domain, expected, nodb_factories, settings):
|
||||
if not domain:
|
||||
domain = settings.FEDERATION_HOSTNAME
|
||||
|
||||
actor = nodb_factories["federation.Actor"](
|
||||
preferred_username=username, domain=domain
|
||||
)
|
||||
assert actor.is_system is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"username,domain,expected",
|
||||
[
|
||||
("test", "wrongdomain.com", None),
|
||||
("notsystem", "", None),
|
||||
("test", "", actors.SYSTEM_ACTORS["test"]),
|
||||
],
|
||||
)
|
||||
def test_actor_system_conf(username, domain, expected, nodb_factories, settings):
|
||||
if not domain:
|
||||
domain = settings.FEDERATION_HOSTNAME
|
||||
actor = nodb_factories["federation.Actor"](
|
||||
preferred_username=username, domain=domain
|
||||
)
|
||||
assert actor.system_conf == expected
|
||||
|
||||
|
||||
@pytest.mark.skip("Refactoring in progress")
|
||||
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)
|
||||
serializer = serializers.ActivitySerializer(data=activity)
|
||||
assert serializer.is_valid()
|
||||
actors.SYSTEM_ACTORS["test"].handle(activity, actor)
|
||||
handler.assert_called_once_with(activity, actor)
|
||||
|
|
|
@ -2,20 +2,7 @@ import pytest
|
|||
from django.core.paginator import Paginator
|
||||
from django.urls import reverse
|
||||
|
||||
from funkwhale_api.federation import actors, serializers, webfinger
|
||||
|
||||
|
||||
@pytest.mark.parametrize("system_actor", actors.SYSTEM_ACTORS.keys())
|
||||
def test_instance_actors(system_actor, db, api_client):
|
||||
actor = actors.SYSTEM_ACTORS[system_actor].get_actor_instance()
|
||||
url = reverse("federation:instance-actors-detail", kwargs={"actor": system_actor})
|
||||
response = api_client.get(url)
|
||||
serializer = serializers.ActorSerializer(actor)
|
||||
|
||||
if system_actor == "library":
|
||||
response.data.pop("url")
|
||||
assert response.status_code == 200
|
||||
assert response.data == serializer.data
|
||||
from funkwhale_api.federation import serializers, webfinger
|
||||
|
||||
|
||||
def test_wellknown_webfinger_validates_resource(db, api_client, settings, mocker):
|
||||
|
@ -29,22 +16,6 @@ def test_wellknown_webfinger_validates_resource(db, api_client, settings, mocker
|
|||
assert response.data["errors"]["resource"] == ("Missing webfinger resource type")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("system_actor", actors.SYSTEM_ACTORS.keys())
|
||||
def test_wellknown_webfinger_system(system_actor, db, api_client, settings, mocker):
|
||||
actor = actors.SYSTEM_ACTORS[system_actor].get_actor_instance()
|
||||
url = reverse("federation:well-known-webfinger")
|
||||
response = api_client.get(
|
||||
url,
|
||||
data={"resource": "acct:{}".format(actor.webfinger_subject)},
|
||||
HTTP_ACCEPT="application/jrd+json",
|
||||
)
|
||||
serializer = serializers.ActorWebfingerSerializer(actor)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response["Content-Type"] == "application/jrd+json"
|
||||
assert response.data == serializer.data
|
||||
|
||||
|
||||
def test_wellknown_nodeinfo(db, preferences, api_client, settings):
|
||||
expected = {
|
||||
"links": [
|
||||
|
|
|
@ -452,11 +452,6 @@ def test_get_audio_data(factories):
|
|||
assert result == {"duration": 229, "bitrate": 128000, "size": 3459481}
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Refactoring in progress")
|
||||
def test_library_viewable_by():
|
||||
assert False
|
||||
|
||||
|
||||
def test_library_queryset_with_follows(factories):
|
||||
library1 = factories["music.Library"]()
|
||||
library2 = factories["music.Library"]()
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Refactoring in progress")
|
||||
def test_can_bind_import_batch_to_request(factories):
|
||||
request = factories["requests.ImportRequest"]()
|
||||
|
||||
assert request.status == "pending"
|
||||
|
||||
# when we create the import, we consider the request as accepted
|
||||
batch = factories["music.ImportBatch"](import_request=request)
|
||||
request.refresh_from_db()
|
||||
|
||||
assert request.status == "accepted"
|
||||
|
||||
# now, the batch is finished, therefore the request status should be
|
||||
# imported
|
||||
batch.status = "finished"
|
||||
batch.save(update_fields=["status"])
|
||||
request.refresh_from_db()
|
||||
|
||||
assert request.status == "imported"
|
|
@ -33,7 +33,7 @@ def test_import_with_multiple_argument(factories, mocker):
|
|||
path1 = os.path.join(DATA_DIR, "dummy_file.ogg")
|
||||
path2 = os.path.join(DATA_DIR, "utf8-éà◌.ogg")
|
||||
mocked_filter = mocker.patch(
|
||||
"funkwhale_api.providers.audiofile.management.commands.import_files.Command.filter_matching",
|
||||
"funkwhale_api.music.management.commands.import_files.Command.filter_matching",
|
||||
return_value=({"new": [], "skipped": []}),
|
||||
)
|
||||
call_command("import_files", str(library.uuid), path1, path2, interactive=False)
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
Todo:
|
||||
|
||||
- upload utilisateur
|
||||
- gestion des doublons ? Si piste uploadée deux fois, on fait quoi:
|
||||
- On rajoute un lien entre la piste existante et la bibliothèque de l'utilisateur
|
||||
- L'utilisateur peut forcer l'upload de SA piste
|
||||
- Comment on gère l'affichage : une piste peut ne pas être jouable
|
||||
- more tests about:
|
||||
- replacing
|
||||
- deletion
|
||||
- permissions
|
||||
|
||||
- un utilisateur envoie une piste:
|
||||
- pas de problème: au pire les métadonnées ne sont pas bonnes mais ce n'est pas très grave
|
||||
- on incite à tagguer avec picard / musicbrainz
|
||||
- en cas de conflit (piste sans tag, mais le nom de l'artiste existe avec un ID musicbrainz, par exemple):
|
||||
on fournit à l'utilisateur le choix (binder à l'artiste musicbrainz), ou créer un artiste séparé
|
||||
on peut aussi tenter des trucs plus intelligents à base de matching sur les noms de pistes, mais dans un second temps
|
||||
|
||||
- Un créateur envoie une piste:
|
||||
- il crée un profil avec son nom d'artiste, des liens vers ses différents profils (youtube, etc).
|
||||
- on peut lui fournir un snippet à inclure sur ses profils "Funkwhale: http://creator.url" pour servir à valider
|
||||
- les instances fédérées pourront donc faire la vérification elles-mêmes
|
||||
- à l'upload, il a un formulaire spécial ou il déclare bien être le créateur des pistes et avoir les droits
|
||||
- on ne tente pas d'être smart : il faut que les données soient fiables et éditables par le créateur avant publication !
|
||||
|
||||
|
||||
Jour 2:
|
||||
|
||||
- on bind les fichiers aux bibliothèques
|
||||
- on ne dédoublonne pas, trop compliqué
|
||||
- en fédé, quand on scanne, on crée les track files, du coup plus besoin d'import manuel
|
||||
- dans le script de migration, gérer le cas dess trucs importés via la fédé
|
||||
|
||||
|
||||
Todo:
|
||||
|
||||
- tester le remplacement
|
||||
- skipper les tracks qui sont déjà dans une autre bibliothèque
|
||||
- gestion d'erreur plus poussée
|
||||
- gérer les radios
|
||||
- tester permission sur la fédé
|
||||
- tester qu'on ne sert que les bibliothèques locales
|
||||
- virer au maximum la logique custom pour les acteurs systèmes
|
||||
- utiliser le vrai champs durée d'activitystream pour l'audio
|
||||
- shared inbox url
|
||||
- vue pour servir les bibliothèques:
|
||||
|
||||
- logique de scan:
|
||||
- pouvoir lancer, mettre en pause, interrompre un scan
|
||||
- avoir des infos sur le déroulement du scan
|
Ładowanie…
Reference in New Issue