Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • funkwhale/funkwhale
  • Luclu7/funkwhale
  • mbothorel/funkwhale
  • EorlBruder/funkwhale
  • tcit/funkwhale
  • JocelynDelalande/funkwhale
  • eneiluj/funkwhale
  • reg/funkwhale
  • ButterflyOfFire/funkwhale
  • m4sk1n/funkwhale
  • wxcafe/funkwhale
  • andybalaam/funkwhale
  • jcgruenhage/funkwhale
  • pblayo/funkwhale
  • joshuaboniface/funkwhale
  • n3ddy/funkwhale
  • gegeweb/funkwhale
  • tohojo/funkwhale
  • emillumine/funkwhale
  • Te-k/funkwhale
  • asaintgenis/funkwhale
  • anoadragon453/funkwhale
  • Sakada/funkwhale
  • ilianaw/funkwhale
  • l4p1n/funkwhale
  • pnizet/funkwhale
  • dante383/funkwhale
  • interfect/funkwhale
  • akhardya/funkwhale
  • svfusion/funkwhale
  • noplanman/funkwhale
  • nykopol/funkwhale
  • roipoussiere/funkwhale
  • Von/funkwhale
  • aurieh/funkwhale
  • icaria36/funkwhale
  • floreal/funkwhale
  • paulwalko/funkwhale
  • comradekingu/funkwhale
  • FurryJulie/funkwhale
  • Legolars99/funkwhale
  • Vierkantor/funkwhale
  • zachhats/funkwhale
  • heyjake/funkwhale
  • sn0w/funkwhale
  • jvoisin/funkwhale
  • gordon/funkwhale
  • Alexander/funkwhale
  • bignose/funkwhale
  • qasim.ali/funkwhale
  • fakegit/funkwhale
  • Kxze/funkwhale
  • stenstad/funkwhale
  • creak/funkwhale
  • Kaze/funkwhale
  • Tixie/funkwhale
  • IISergII/funkwhale
  • lfuelling/funkwhale
  • nhaddag/funkwhale
  • yoasif/funkwhale
  • ifischer/funkwhale
  • keslerm/funkwhale
  • flupe/funkwhale
  • petitminion/funkwhale
  • ariasuni/funkwhale
  • ollie/funkwhale
  • ngaumont/funkwhale
  • techknowlogick/funkwhale
  • Shleeble/funkwhale
  • theflyingfrog/funkwhale
  • jonatron/funkwhale
  • neobrain/funkwhale
  • eorn/funkwhale
  • KokaKiwi/funkwhale
  • u1-liquid/funkwhale
  • marzzzello/funkwhale
  • sirenwatcher/funkwhale
  • newer027/funkwhale
  • codl/funkwhale
  • Zwordi/funkwhale
  • gisforgabriel/funkwhale
  • iuriatan/funkwhale
  • simon/funkwhale
  • bheesham/funkwhale
  • zeoses/funkwhale
  • accraze/funkwhale
  • meliurwen/funkwhale
  • divadsn/funkwhale
  • Etua/funkwhale
  • sdrik/funkwhale
  • Soran/funkwhale
  • kuba-orlik/funkwhale
  • cristianvogel/funkwhale
  • Forceu/funkwhale
  • jeff/funkwhale
  • der_scheibenhacker/funkwhale
  • owlnical/funkwhale
  • jovuit/funkwhale
  • SilverFox15/funkwhale
  • phw/funkwhale
  • mayhem/funkwhale
  • sridhar/funkwhale
  • stromlin/funkwhale
  • rrrnld/funkwhale
  • nitaibezerra/funkwhale
  • jaller94/funkwhale
  • pcouy/funkwhale
  • eduxstad/funkwhale
  • codingHahn/funkwhale
  • captain/funkwhale
  • polyedre/funkwhale
  • leishenailong/funkwhale
  • ccritter/funkwhale
  • lnceballosz/funkwhale
  • fpiesche/funkwhale
  • Fanyx/funkwhale
  • markusblogde/funkwhale
  • Firobe/funkwhale
  • devilcius/funkwhale
  • freaktechnik/funkwhale
  • blopware/funkwhale
  • cone/funkwhale
  • thanksd/funkwhale
  • vachan-maker/funkwhale
  • bbenti/funkwhale
  • tarator/funkwhale
  • prplecake/funkwhale
  • DMarzal/funkwhale
  • lullis/funkwhale
  • hanacgr/funkwhale
  • albjeremias/funkwhale
  • xeruf/funkwhale
  • llelite/funkwhale
  • RoiArthurB/funkwhale
  • cloo/funkwhale
  • nztvar/funkwhale
  • Keunes/funkwhale
  • petitminion/funkwhale-petitminion
  • m-idler/funkwhale
  • SkyLeite/funkwhale
140 results
Select Git revision
Show changes
Showing
with 778 additions and 157 deletions
import requests.exceptions
from django.conf import settings
from django.db import transaction
from django.db.models import Count, Q
from rest_framework import decorators
from rest_framework import mixins
from rest_framework import permissions
from rest_framework import response
from rest_framework import viewsets
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import decorators, mixins, permissions, response, viewsets
from rest_framework.exceptions import NotFound as RestNotFound
from funkwhale_api.common import preferences
from funkwhale_api.common import utils as common_utils
from funkwhale_api.common.permissions import ConditionalAuthentication
from funkwhale_api.music import models as music_models
from funkwhale_api.music import serializers as music_serializers
from funkwhale_api.music import views as music_views
from funkwhale_api.users.oauth import permissions as oauth_permissions
from . import activity
from . import api_serializers
from . import exceptions
from . import filters
from . import models
from . import routes
from . import serializers
from . import tasks
from . import utils
from . import (
activity,
api_serializers,
exceptions,
filters,
models,
routes,
serializers,
tasks,
utils,
)
@transaction.atomic
......@@ -38,6 +37,10 @@ def update_follow(follow, approved):
routes.outbox.dispatch({"type": "Reject"}, context={"follow": follow})
@extend_schema_view(
list=extend_schema(operation_id="get_federation_library_follows"),
create=extend_schema(operation_id="create_federation_library_follow"),
)
class LibraryFollowViewSet(
mixins.CreateModelMixin,
mixins.ListModelMixin,
......@@ -57,6 +60,14 @@ class LibraryFollowViewSet(
filterset_class = filters.LibraryFollowFilter
ordering_fields = ("creation_date",)
@extend_schema(operation_id="get_federation_library_follow")
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
@extend_schema(operation_id="delete_federation_library_follow")
def destroy(self, request, uuid=None):
return super().destroy(request, uuid)
def get_queryset(self):
qs = super().get_queryset()
return qs.filter(actor=self.request.user.actor).exclude(approved=False)
......@@ -77,6 +88,10 @@ class LibraryFollowViewSet(
context["actor"] = self.request.user.actor
return context
@extend_schema(
operation_id="accept_federation_library_follow",
responses={404: None, 204: None},
)
@decorators.action(methods=["post"], detail=True)
def accept(self, request, *args, **kwargs):
try:
......@@ -88,6 +103,7 @@ class LibraryFollowViewSet(
update_follow(follow, approved=True)
return response.Response(status=204)
@extend_schema(operation_id="reject_federation_library_follow")
@decorators.action(methods=["post"], detail=True)
def reject(self, request, *args, **kwargs):
try:
......@@ -100,6 +116,7 @@ class LibraryFollowViewSet(
update_follow(follow, approved=False)
return response.Response(status=204)
@extend_schema(operation_id="get_all_federation_library_follows")
@decorators.action(methods=["get"], detail=False)
def all(self, request, *args, **kwargs):
"""
......@@ -174,12 +191,12 @@ class LibraryViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
)
except requests.exceptions.RequestException as e:
return response.Response(
{"detail": "Error while fetching the library: {}".format(str(e))},
{"detail": f"Error while fetching the library: {str(e)}"},
status=400,
)
except serializers.serializers.ValidationError as e:
return response.Response(
{"detail": "Invalid data in remote library: {}".format(str(e))},
{"detail": f"Invalid data in remote library: {str(e)}"},
status=400,
)
serializer = self.serializer_class(library)
......@@ -192,7 +209,6 @@ class InboxItemViewSet(
mixins.RetrieveModelMixin,
viewsets.GenericViewSet,
):
queryset = (
models.InboxItem.objects.select_related("activity__actor")
.prefetch_related("activity__object", "activity__target")
......@@ -223,7 +239,6 @@ class InboxItemViewSet(
class FetchViewSet(
mixins.CreateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
queryset = models.Fetch.objects.select_related("actor")
serializer_class = api_serializers.FetchSerializer
permission_classes = [permissions.IsAuthenticated]
......@@ -275,7 +290,12 @@ class ActorViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
def get_object(self):
queryset = self.get_queryset()
username, domain = self.kwargs["full_username"].split("@", 1)
try:
return queryset.get(preferred_username=username, domain_id=domain)
except models.Actor.DoesNotExist:
raise RestNotFound(
detail=f"Actor {username}@{domain} not found",
)
def get_queryset(self):
qs = super().get_queryset()
......@@ -288,8 +308,115 @@ class ActorViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
qs = qs.filter(query)
return qs
libraries = decorators.action(methods=["get"], detail=True)(
libraries = decorators.action(
methods=["get"],
detail=True,
serializer_class=music_serializers.LibraryForOwnerSerializer,
)(
music_views.get_libraries(
filter_uploads=lambda o, uploads: uploads.filter(library__actor=o)
)
)
@extend_schema_view(
list=extend_schema(operation_id="get_federation_received_follows"),
create=extend_schema(operation_id="create_federation_user_follow"),
)
class UserFollowViewSet(
mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
lookup_field = "uuid"
queryset = (
models.Follow.objects.all()
.order_by("-creation_date")
.select_related("actor", "target")
.filter(actor__type="Person")
)
serializer_class = api_serializers.FollowSerializer
permission_classes = [oauth_permissions.ScopePermission]
required_scope = "follows"
ordering_fields = ("creation_date",)
@extend_schema(operation_id="get_federation_user_follow")
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
@extend_schema(operation_id="delete_federation_user_follow")
def destroy(self, request, uuid=None):
return super().destroy(request, uuid)
def get_queryset(self):
qs = super().get_queryset()
return qs.filter(
Q(target=self.request.user.actor) | Q(actor=self.request.user.actor)
).exclude(approved=False)
def perform_create(self, serializer):
follow = serializer.save(actor=self.request.user.actor)
routes.outbox.dispatch({"type": "Follow"}, context={"follow": follow})
@transaction.atomic
def perform_destroy(self, instance):
routes.outbox.dispatch(
{"type": "Undo", "object": {"type": "Follow"}}, context={"follow": instance}
)
instance.delete()
def get_serializer_context(self):
context = super().get_serializer_context()
context["actor"] = self.request.user.actor
return context
@extend_schema(
operation_id="accept_federation_user_follow",
responses={404: None, 204: None},
)
@decorators.action(methods=["post"], detail=True)
def accept(self, request, *args, **kwargs):
try:
follow = self.queryset.get(
target=self.request.user.actor, uuid=kwargs["uuid"]
)
except models.Follow.DoesNotExist:
return response.Response({}, status=404)
update_follow(follow, approved=True)
return response.Response(status=204)
@extend_schema(operation_id="reject_federation_user_follow")
@decorators.action(methods=["post"], detail=True)
def reject(self, request, *args, **kwargs):
try:
follow = self.queryset.get(
target=self.request.user.actor, uuid=kwargs["uuid"]
)
except models.Follow.DoesNotExist:
return response.Response({}, status=404)
update_follow(follow, approved=False)
return response.Response(status=204)
@extend_schema(operation_id="get_all_federation_library_follows")
@decorators.action(methods=["get"], detail=False)
def all(self, request, *args, **kwargs):
"""
Return all the subscriptions of the current user, with only limited data
to have a performant endpoint and avoid lots of queries just to display
subscription status in the UI
"""
follows = list(
self.get_queryset().values_list("uuid", "target__fid", "approved")
)
payload = {
"results": [
{"uuid": str(u[0]), "actor": str(u[1]), "approved": u[2]}
for u in follows
],
"count": len(follows),
}
return response.Response(payload, status=200)
import cryptography
import logging
import datetime
import logging
import urllib.parse
import cryptography
from django.contrib.auth.models import AnonymousUser
from django.utils import timezone
from rest_framework import authentication
from rest_framework import exceptions as rest_exceptions
from rest_framework import authentication, exceptions as rest_exceptions
from funkwhale_api.common import preferences
from funkwhale_api.moderation import models as moderation_models
from . import actors, exceptions, keys, models, signing, tasks, utils
from . import actors, exceptions, keys, models, signing, tasks, utils
logger = logging.getLogger(__name__)
......@@ -53,7 +55,9 @@ class SignatureAuthentication(authentication.BaseAuthentication):
actor = actors.get_actor(actor_url)
except Exception as e:
logger.info(
"Discarding HTTP request from actor/domain %s, %s", actor_url, str(e),
"Discarding HTTP request from actor/domain %s, %s",
actor_url,
str(e),
)
raise rest_exceptions.AuthenticationFailed(
"Cannot fetch remote actor to authenticate signature"
......@@ -77,6 +81,7 @@ class SignatureAuthentication(authentication.BaseAuthentication):
fetch_delay = 24 * 3600
now = timezone.now()
last_fetch = actor.domain.nodeinfo_fetch_date
if not actor.domain.is_local:
if not last_fetch or (
last_fetch < (now - datetime.timedelta(seconds=fetch_delay))
):
......
......@@ -293,7 +293,11 @@ CONTEXTS = [
"Album": "fw:Album",
"Track": "fw:Track",
"Artist": "fw:Artist",
"ArtistCredit": "fw:ArtistCredit",
"Library": "fw:Library",
"Playlist": "fw:Playlist",
"PlaylistTrack": "fw:PlaylistTrack",
"AudioCollection": "fw:AudioCollection",
"bitrate": {"@id": "fw:bitrate", "@type": "xsd:nonNegativeInteger"},
"size": {"@id": "fw:size", "@type": "xsd:nonNegativeInteger"},
"position": {"@id": "fw:position", "@type": "xsd:nonNegativeInteger"},
......@@ -302,13 +306,23 @@ CONTEXTS = [
"track": {"@id": "fw:track", "@type": "@id"},
"cover": {"@id": "fw:cover", "@type": "as:Link"},
"album": {"@id": "fw:album", "@type": "@id"},
"artist": {"@id": "fw:artist", "@type": "@id"},
"artists": {"@id": "fw:artists", "@type": "@id", "@container": "@list"},
"artist_credit": {
"@id": "fw:artist_credit",
"@type": "@id",
"@container": "@list",
},
"joinphrase": {"@id": "fw:joinphrase", "@type": "xsd:string"},
"credit": {"@id": "fw:credit", "@type": "xsd:string"},
"index": {"@id": "fw:index", "@type": "xsd:nonNegativeInteger"},
"released": {"@id": "fw:released", "@type": "xsd:date"},
"musicbrainzId": "fw:musicbrainzId",
"license": {"@id": "fw:license", "@type": "@id"},
"copyright": "fw:copyright",
"category": "schema:category",
"language": "schema:inLanguage",
"playlist": {"@id": "fw:playlist", "@type": "@id"},
}
},
},
......@@ -362,14 +376,14 @@ class NS:
def __getattr__(self, key):
if key not in self.conf["document"]["@context"]:
raise AttributeError(
"{} is not a valid property of context {}".format(key, self.baseUrl)
f"{key} is not a valid property of context {self.baseUrl}"
)
return self.baseUrl + key
class NoopContext:
def __getattr__(self, key):
return "_:{}".format(key)
return f"_:{key}"
NOOP = NoopContext()
......
from django.db import transaction
from rest_framework import decorators
from rest_framework import permissions
from rest_framework import response
from rest_framework import status
from drf_spectacular.utils import OpenApiParameter, extend_schema
from rest_framework import decorators, permissions, response, status
from funkwhale_api.common import utils as common_utils
from . import api_serializers
from . import filters
from . import models
from . import tasks
from . import utils
from . import api_serializers, filters, models, tasks, utils
def fetches_route():
......@@ -42,8 +35,16 @@ def fetches_route():
serializer = api_serializers.FetchSerializer(fetch)
return response.Response(serializer.data, status=status.HTTP_201_CREATED)
return decorators.action(
return extend_schema(methods=["post"], responses=api_serializers.FetchSerializer())(
extend_schema(
methods=["get"],
responses=api_serializers.FetchSerializer(many=True),
parameters=[OpenApiParameter("id", location="query", exclude=True)],
)(
decorators.action(
methods=["get", "post"],
detail=True,
permission_classes=[permissions.IsAuthenticated],
)(fetches)
)
)
......@@ -2,12 +2,13 @@ import uuid
import factory
import requests
import requests_http_signature
import requests_http_message_signatures
from django.conf import settings
from django.db.models.signals import post_save
from django.utils import timezone
from django.utils.http import http_date
from funkwhale_api.factories import registry, NoUpdateOnCreate
from funkwhale_api.factories import NoUpdateOnCreate, registry
from funkwhale_api.users import factories as user_factories
from . import keys, models
......@@ -20,11 +21,10 @@ class SignatureAuthFactory(factory.Factory):
algorithm = "rsa-sha256"
key = factory.LazyFunction(lambda: keys.get_key_pair()[0])
key_id = factory.Faker("url")
use_auth_header = False
headers = ["(request-target)", "user-agent", "host", "date", "accept"]
class Meta:
model = requests_http_signature.HTTPSignatureAuth
model = requests_http_message_signatures.HTTPSignatureHeaderAuth
@registry.register(name="federation.SignedRequest")
......@@ -71,6 +71,8 @@ class DomainFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
name = factory.Faker("domain_name")
nodeinfo_fetch_date = factory.LazyFunction(lambda: timezone.now())
allowed = None
reachable = True
last_successful_contact = None
class Meta:
model = "federation.Domain"
......@@ -105,7 +107,7 @@ class ActorFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
summary = factory.Faker("paragraph")
domain = factory.SubFactory(DomainFactory)
fid = factory.LazyAttribute(
lambda o: "https://{}/users/{}".format(o.domain.name, o.preferred_username)
lambda o: f"https://{o.domain.name}/users/{o.preferred_username}"
)
followers_url = factory.LazyAttribute(
lambda o: "https://{}/users/{}followers".format(
......@@ -127,9 +129,6 @@ class ActorFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
class Meta:
model = models.Actor
class Params:
with_real_keys = factory.Trait(keys=factory.LazyFunction(keys.get_key_pair),)
@factory.post_generation
def local(self, create, extracted, **kwargs):
if not extracted and not kwargs:
......@@ -139,7 +138,7 @@ class ActorFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
self.domain = models.Domain.objects.get_or_create(
name=settings.FEDERATION_HOSTNAME
)[0]
self.fid = "https://{}/actors/{}".format(self.domain, self.preferred_username)
self.fid = f"https://{self.domain}/actors/{self.preferred_username}"
self.save(update_fields=["domain", "fid"])
if not create:
if extracted and hasattr(extracted, "pk"):
......@@ -149,8 +148,26 @@ class ActorFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
if extracted and hasattr(extracted, "pk"):
extracted.actor = self
extracted.save(update_fields=["user"])
else:
@factory.post_generation
def user(self, create, extracted, **kwargs):
"""
Handle the creation or assignment of the related user instance.
If `actor__user` is passed, it will be linked; otherwise, no user is created.
"""
from funkwhale_api.users.factories import UserFactory
if not create:
return
if extracted: # If a User instance is provided
extracted.actor = self
extracted.save(update_fields=["actor"])
elif kwargs:
# Create a User linked to this Actor
self.user = UserFactory(actor=self, **kwargs)
else:
self.user = UserFactory(actor=self)
@registry.register
......@@ -164,25 +181,37 @@ class FollowFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
class Params:
local = factory.Trait(actor=factory.SubFactory(ActorFactory, local=True))
@classmethod
@factory.django.mute_signals(post_save)
def _create(cls, model_class, *args, **kwargs):
"""
Overrides Factory Boy's object creation to suppress post_save signals
only during this factory's create(). Needed because Follow creation trigger a remote library fetch
"""
return super()._create(model_class, *args, **kwargs)
@registry.register
class MusicLibraryFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
uuid = factory.Faker("uuid4")
actor = factory.SubFactory(ActorFactory)
privacy_level = "me"
name = factory.Faker("sentence")
description = factory.Faker("sentence")
name = privacy_level
uploads_count = 0
fid = factory.Faker("federation_url")
followers_url = factory.LazyAttribute(
lambda o: o.fid + "/followers" if o.fid else None
)
class Meta:
model = "music.Library"
class Params:
local = factory.Trait(
fid=None, actor=factory.SubFactory(ActorFactory, local=True)
fid=factory.Faker(
"federation_url",
local=True,
prefix="federation/music/libraries",
obj_uuid=factory.SelfAttribute("..uuid"),
),
actor=factory.SubFactory(ActorFactory, local=True),
)
......@@ -297,13 +326,13 @@ class NoteFactory(factory.Factory):
@registry.register(name="federation.AudioMetadata")
class AudioMetadataFactory(factory.Factory):
recording = factory.LazyAttribute(
lambda o: "https://musicbrainz.org/recording/{}".format(uuid.uuid4())
lambda o: f"https://musicbrainz.org/recording/{uuid.uuid4()}"
)
artist = factory.LazyAttribute(
lambda o: "https://musicbrainz.org/artist/{}".format(uuid.uuid4())
lambda o: f"https://musicbrainz.org/artist/{uuid.uuid4()}"
)
release = factory.LazyAttribute(
lambda o: "https://musicbrainz.org/release/{}".format(uuid.uuid4())
lambda o: f"https://musicbrainz.org/release/{uuid.uuid4()}"
)
bitrate = 42
length = 43
......
import django_filters
from rest_framework import serializers
from . import models
from . import utils
from . import models, utils
class ActorRelatedField(serializers.EmailField):
......
import aiohttp
import asyncio
import functools
import logging
import aiohttp
import pyld.documentloader.requests
import pyld.jsonld
from django.conf import settings
import pyld.documentloader.requests
from rest_framework import serializers
from rest_framework.fields import empty
from . import contexts
logger = logging.getLogger(__name__)
def cached_contexts(loader):
functools.wraps(loader)
......@@ -56,7 +60,6 @@ def expand(doc, options=None, default_contexts=["AS", "FW", "SEC"]):
except KeyError:
# Nothing to do here if no context is available at all
pass
result = pyld.jsonld.expand(doc, options=options)
try:
# jsonld.expand returns a list, which is useless for us
......@@ -95,7 +98,7 @@ async def fetch_many(*ids, references=None):
"""
Given a list of object ids, will fetch the remote
representations for those objects, expand them
and return a dictionnary with id as the key and expanded document as the values
and return a dictionary with id as the key and expanded document as the values
"""
ids = set(ids)
results = references if references is not None else {}
......@@ -121,7 +124,7 @@ DEFAULT_PREPARE_CONFIG = {
def dereference(value, references):
"""
Given a payload and a dictonary containing ids and objects, will replace
Given a payload and a dictionary containing ids and objects, will replace
all the matching objects in the payload by the one in the references dictionary.
"""
......@@ -150,7 +153,6 @@ def dereference(value, references):
def get_value(value, keep=None, attr=None):
if keep == "first":
value = value[0]
if attr:
......@@ -165,10 +167,10 @@ def get_value(value, keep=None, attr=None):
def prepare_for_serializer(payload, config, fallbacks={}):
"""
Json-ld payloads, as returned by expand are quite complex to handle, because
every attr is basically a list of dictionnaries. To make code simpler,
every attr is basically a list of dictionaries. To make code simpler,
we use this function to clean the payload a little bit, base on the config object.
Config is a dictionnary, with keys being serializer field names, and values
Config is a dictionary, with keys being serializer field names, and values
being dictionaries describing how to handle this field.
"""
final_payload = {}
......@@ -188,11 +190,12 @@ def prepare_for_serializer(payload, config, fallbacks={}):
value = noop
if not aliases:
continue
for a in aliases:
try:
value = get_value(
payload[a["property"]], keep=a.get("keep"), attr=a.get("attr"),
payload[a["property"]],
keep=a.get("keep"),
attr=a.get("attr"),
)
except (IndexError, KeyError):
continue
......@@ -247,14 +250,13 @@ class JsonLdSerializer(serializers.Serializer):
def run_validation(self, data=empty):
if data and data is not empty:
self.jsonld_context = data.get("@context", [])
if self.context.get("expand", self.jsonld_expand):
try:
data = expand(data)
except ValueError as e:
raise serializers.ValidationError(
"{} is not a valid jsonld document: {}".format(data, e)
f"{data} is not a valid jsonld document: {e}"
)
try:
config = self.Meta.jsonld_mapping
......@@ -275,11 +277,11 @@ class JsonLdSerializer(serializers.Serializer):
for field in dereferenced_fields:
for i in get_ids(data[field]):
dereferenced_ids.add(i)
if dereferenced_ids:
try:
loop = asyncio.get_event_loop()
except RuntimeError:
except RuntimeError as exception:
logger.debug(exception)
loop = asyncio.new_event_loop()
references = self.context.setdefault("references", {})
loop.run_until_complete(
......
import re
import urllib.parse
from django.conf import settings
from cryptography.hazmat.backends import default_backend as crypto_default_backend
from cryptography.hazmat.primitives import serialization as crypto_serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from django.conf import settings
KEY_ID_REGEX = re.compile(r"keyId=\"(?P<id>.*)\"")
......
......@@ -9,7 +9,9 @@ def get_library_data(library_url, actor):
auth = signing.get_auth(actor.private_key, actor.private_key_id)
try:
response = session.get_session().get(
library_url, auth=auth, headers={"Accept": "application/activity+json"},
library_url,
auth=auth,
headers={"Accept": "application/activity+json"},
)
except requests.ConnectionError:
return {"errors": ["This library is not reachable"]}
......@@ -19,7 +21,7 @@ def get_library_data(library_url, actor):
elif scode == 403:
return {"errors": ["Permission denied while scanning library"]}
elif scode >= 400:
return {"errors": ["Error {} while fetching the library".format(scode)]}
return {"errors": [f"Error {scode} while fetching the library"]}
serializer = serializers.LibrarySerializer(data=response.json())
if not serializer.is_valid():
return {"errors": ["Invalid ActivityPub response from remote library"]}
......@@ -30,7 +32,9 @@ def get_library_data(library_url, actor):
def get_library_page(library, page_url, actor):
auth = signing.get_auth(actor.private_key, actor.private_key_id)
response = session.get_session().get(
page_url, auth=auth, headers={"Accept": "application/activity+json"},
page_url,
auth=auth,
headers={"Accept": "application/activity+json"},
)
serializer = serializers.CollectionPageSerializer(
data=response.json(),
......
......@@ -4,13 +4,12 @@ from funkwhale_api.common import utils
from funkwhale_api.federation import models as federation_models
from funkwhale_api.music import models as music_models
MODELS = [
(music_models.Artist, ["fid"]),
(music_models.Album, ["fid"]),
(music_models.Track, ["fid"]),
(music_models.Upload, ["fid"]),
(music_models.Library, ["fid", "followers_url"]),
(music_models.Library, ["fid"]),
(
federation_models.Actor,
[
......@@ -31,7 +30,7 @@ MODELS = [
class Command(BaseCommand):
help = """
Find and replace wrong protocal/domain in local federation ids.
Find and replace wrong protocol/domain in local federation ids.
Use with caution and only if you know what you are doing.
"""
......@@ -68,9 +67,7 @@ class Command(BaseCommand):
for kls, fields in MODELS:
results[kls] = {}
for field in fields:
candidates = kls.objects.filter(
**{"{}__startswith".format(field): old_prefix}
)
candidates = kls.objects.filter(**{f"{field}__startswith": old_prefix})
results[kls][field] = candidates.count()
total = sum([t for k in results.values() for t in k.values()])
......@@ -93,9 +90,7 @@ class Command(BaseCommand):
)
else:
self.stdout.write(
"No objects found with prefix {}, exiting.".format(old_prefix)
)
self.stdout.write(f"No objects found with prefix {old_prefix}, exiting.")
return
if options["dry_run"]:
self.stdout.write(
......@@ -113,9 +108,7 @@ class Command(BaseCommand):
for kls, fields in results.items():
for field, count in fields.items():
self.stdout.write(
"Replacing {} on {} {}…".format(field, count, kls._meta.label)
)
self.stdout.write(f"Replacing {field} on {count} {kls._meta.label}")
candidates = kls.objects.all()
utils.replace_prefix(candidates, field, old=old_prefix, new=new_prefix)
self.stdout.write("")
......
......@@ -75,7 +75,7 @@ class Migration(migrations.Migration):
"last_fetch_date",
models.DateTimeField(default=django.utils.timezone.now),
),
("manually_approves_followers", models.NullBooleanField(default=None)),
("manually_approves_followers", models.BooleanField(default=None, null=True)),
],
)
]
......@@ -77,7 +77,7 @@ class Migration(migrations.Migration):
models.DateTimeField(default=django.utils.timezone.now),
),
("modification_date", models.DateTimeField(auto_now=True)),
("approved", models.NullBooleanField(default=None)),
("approved", models.BooleanField(default=None, null=True)),
(
"actor",
models.ForeignKey(
......
......@@ -14,7 +14,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name="follow",
name="approved",
field=models.NullBooleanField(default=None),
field=models.BooleanField(default=None, null=True),
),
migrations.AddField(
model_name="library",
......
......@@ -43,7 +43,7 @@ class Migration(migrations.Migration):
"creation_date",
models.DateTimeField(default=django.utils.timezone.now),
),
("delivered", models.NullBooleanField(default=None)),
("delivered", models.BooleanField(default=None, null=True)),
("delivered_date", models.DateTimeField(blank=True, null=True)),
],
),
......@@ -69,7 +69,7 @@ class Migration(migrations.Migration):
models.DateTimeField(default=django.utils.timezone.now),
),
("modification_date", models.DateTimeField(auto_now=True)),
("approved", models.NullBooleanField(default=None)),
("approved", models.BooleanField(default=None, null=True)),
],
),
migrations.RenameField("actor", "url", "fid"),
......
# Generated by Django 3.2.13 on 2022-06-27 19:15
import django.core.serializers.json
from django.db import migrations, models
import funkwhale_api.federation.models
class Migration(migrations.Migration):
dependencies = [
('federation', '0026_public_key_format'),
]
operations = [
migrations.AlterField(
model_name='activity',
name='payload',
field=models.JSONField(blank=True, default=funkwhale_api.federation.models.empty_dict, encoder=django.core.serializers.json.DjangoJSONEncoder, max_length=50000),
),
migrations.AlterField(
model_name='actor',
name='manually_approves_followers',
field=models.BooleanField(default=None, null=True),
),
migrations.AlterField(
model_name='domain',
name='nodeinfo',
field=models.JSONField(blank=True, default=funkwhale_api.federation.models.empty_dict, max_length=50000),
),
migrations.AlterField(
model_name='fetch',
name='detail',
field=models.JSONField(blank=True, default=funkwhale_api.federation.models.empty_dict, encoder=django.core.serializers.json.DjangoJSONEncoder, max_length=50000),
),
migrations.AlterField(
model_name='follow',
name='approved',
field=models.BooleanField(default=None, null=True),
),
migrations.AlterField(
model_name='libraryfollow',
name='approved',
field=models.BooleanField(default=None, null=True),
),
migrations.AlterField(
model_name='librarytrack',
name='metadata',
field=models.JSONField(blank=True, default=funkwhale_api.federation.models.empty_dict, encoder=django.core.serializers.json.DjangoJSONEncoder, max_length=10000),
),
]
# Generated by Django 3.2.16 on 2022-10-27 11:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('federation', '0027_auto_20220627_1915'),
]
operations = [
migrations.AddField(
model_name='domain',
name='last_successful_contact',
field=models.DateTimeField(default=None, null=True),
),
migrations.AddField(
model_name='domain',
name='reachable',
field=models.BooleanField(default=True),
),
]
# Generated by Django 5.1.6 on 2025-08-04 13:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("federation", "0028_auto_20221027_1141"),
("music", "0061_migrate_libraries_to_playlist"),
]
operations = [
migrations.AddField(
model_name="domain",
name="reachable_retries",
field=models.PositiveIntegerField(default=0),
),
]
......@@ -3,16 +3,16 @@ import urllib.parse
import uuid
from django.conf import settings
from django.contrib.postgres.fields import JSONField
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.db.models.signals import post_save, pre_save, post_delete
from django.db.models import JSONField
from django.db.models.signals import post_delete, post_save, pre_save
from django.dispatch import receiver
from django.utils import timezone
from django.urls import reverse
from django.utils import timezone
from funkwhale_api.common import session
from funkwhale_api.common import utils as common_utils
......@@ -30,6 +30,10 @@ TYPE_CHOICES = [
("Service", "Service"),
]
MAX_LENGTHS = {
"ACTOR_NAME": 200,
}
def empty_dict():
return {}
......@@ -48,7 +52,7 @@ class FederationMixin(models.Model):
abstract = True
@property
def is_local(self):
def is_local(self) -> bool:
return federation_utils.is_local(self.fid)
@property
......@@ -76,7 +80,7 @@ class ActorQuerySet(models.QuerySet):
)
qs = qs.annotate(
**{
"_usage_{}".format(s): models.Sum(
f"_usage_{s}": models.Sum(
"libraries__uploads__size", filter=uploads_query
)
}
......@@ -123,7 +127,9 @@ class Domain(models.Model):
)
# are interactions with this domain allowed (only applies when allow-listing is on)
allowed = models.BooleanField(default=None, null=True)
reachable = models.BooleanField(default=True)
last_successful_contact = models.DateTimeField(default=None, null=True)
reachable_retries = models.PositiveIntegerField(default=0)
objects = DomainQuerySet.as_manager()
def __str__(self):
......@@ -172,7 +178,7 @@ class Domain(models.Model):
return data
@property
def is_local(self):
def is_local(self) -> bool:
return self.name == settings.FEDERATION_HOSTNAME
......@@ -187,7 +193,7 @@ class Actor(models.Model):
followers_url = models.URLField(max_length=500, null=True, blank=True)
shared_inbox_url = models.URLField(max_length=500, null=True, blank=True)
type = models.CharField(choices=TYPE_CHOICES, default="Person", max_length=25)
name = models.CharField(max_length=200, null=True, blank=True)
name = models.CharField(max_length=MAX_LENGTHS["ACTOR_NAME"], null=True, blank=True)
domain = models.ForeignKey(Domain, on_delete=models.CASCADE, related_name="actors")
summary = models.CharField(max_length=500, null=True, blank=True)
summary_obj = models.ForeignKey(
......@@ -198,7 +204,7 @@ class Actor(models.Model):
private_key = models.TextField(max_length=5000, null=True, blank=True)
creation_date = models.DateTimeField(default=timezone.now)
last_fetch_date = models.DateTimeField(default=timezone.now)
manually_approves_followers = models.NullBooleanField(default=None)
manually_approves_followers = models.BooleanField(default=None, null=True)
followers = models.ManyToManyField(
to="self",
symmetrical=False,
......@@ -213,7 +219,6 @@ class Actor(models.Model):
on_delete=models.SET_NULL,
related_name="iconed_actor",
)
objects = ActorQuerySet.as_manager()
class Meta:
......@@ -221,34 +226,40 @@ class Actor(models.Model):
verbose_name = "Account"
def get_moderation_url(self):
return "/manage/moderation/accounts/{}".format(self.full_username)
return f"/manage/moderation/accounts/{self.full_username}"
@property
def webfinger_subject(self):
return "{}@{}".format(self.preferred_username, settings.FEDERATION_HOSTNAME)
return f"{self.preferred_username}@{settings.FEDERATION_HOSTNAME}"
@property
def private_key_id(self):
return "{}#main-key".format(self.fid)
return f"{self.fid}#main-key"
@property
def full_username(self):
return "{}@{}".format(self.preferred_username, self.domain_id)
def full_username(self) -> str:
return f"{self.preferred_username}@{self.domain_id}"
def __str__(self):
return "{}@{}".format(self.preferred_username, self.domain_id)
return f"{self.preferred_username}@{self.domain_id}"
@property
def is_local(self):
def is_local(self) -> bool:
return self.domain_id == settings.FEDERATION_HOSTNAME
def get_approved_followers(self):
follows = self.received_follows.filter(approved=True)
return self.followers.filter(pk__in=follows.values_list("actor", flat=True))
def get_approved_followings(self):
follows = self.emitted_follows.filter(approved=True)
return Actor.objects.filter(pk__in=follows.values_list("target", flat=True))
def should_autoapprove_follow(self, actor):
if self.get_channel():
return True
if self.user.privacy_level == "public":
return True
return False
def get_user(self):
......@@ -265,21 +276,21 @@ class Actor(models.Model):
def get_absolute_url(self):
if self.is_local:
return federation_utils.full_url("/@{}".format(self.preferred_username))
return federation_utils.full_url(f"/@{self.preferred_username}")
return self.url or self.fid
def get_current_usage(self):
actor = self.__class__.objects.filter(pk=self.pk).with_current_usage().get()
data = {}
for s in ["draft", "pending", "skipped", "errored", "finished"]:
data[s] = getattr(actor, "_usage_{}".format(s)) or 0
data[s] = getattr(actor, f"_usage_{s}") or 0
data["total"] = sum(data.values())
return data
def get_stats(self):
from funkwhale_api.music import models as music_models
from funkwhale_api.moderation import models as moderation_models
from funkwhale_api.music import models as music_models
data = Actor.objects.filter(pk=self.pk).aggregate(
outbox_activities=models.Count("outbox_activities", distinct=True),
......@@ -336,8 +347,8 @@ class Actor(models.Model):
# matches, we consider the actor has the permission to manage
# the object
domain = self.domain_id
return obj.fid.startswith("http://{}/".format(domain)) or obj.fid.startswith(
"https://{}/".format(domain)
return obj.fid.startswith(f"http://{domain}/") or obj.fid.startswith(
f"https://{domain}/"
)
@property
......@@ -384,8 +395,7 @@ class Fetch(models.Model):
@property
def serializers(self):
from . import contexts
from . import serializers
from . import contexts, serializers
return {
contexts.FW.Artist: [serializers.ArtistSerializer],
......@@ -396,6 +406,7 @@ class Fetch(models.Model):
serializers.ChannelUploadSerializer,
],
contexts.FW.Library: [serializers.LibrarySerializer],
contexts.FW.Playlist: [serializers.PlaylistSerializer],
contexts.AS.Group: [serializers.ActorSerializer],
contexts.AS.Person: [serializers.ActorSerializer],
contexts.AS.Organization: [serializers.ActorSerializer],
......@@ -488,15 +499,13 @@ class AbstractFollow(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
creation_date = models.DateTimeField(default=timezone.now)
modification_date = models.DateTimeField(auto_now=True)
approved = models.NullBooleanField(default=None)
approved = models.BooleanField(default=None, null=True)
class Meta:
abstract = True
def get_federation_id(self):
return federation_utils.full_url(
"{}#follows/{}".format(self.actor.fid, self.uuid)
)
return federation_utils.full_url(f"{self.actor.fid}#follows/{self.uuid}")
class Follow(AbstractFollow):
......@@ -590,7 +599,7 @@ class LibraryTrack(models.Model):
remote_response.raise_for_status()
extension = music_utils.get_ext_from_type(self.audio_mimetype)
title = " - ".join([self.title, self.album_title, self.artist_name])
filename = "{}.{}".format(title, extension)
filename = f"{title}.{extension}"
tmp_file = tempfile.TemporaryFile()
for chunk in r.iter_content(chunk_size=512):
tmp_file.write(chunk)
......@@ -601,6 +610,7 @@ class LibraryTrack(models.Model):
@receiver(pre_save, sender=LibraryFollow)
@receiver(pre_save, sender=Follow)
def set_approved_updated(sender, instance, update_fields, **kwargs):
if not instance.pk or not instance.actor.is_local:
return
......@@ -615,24 +625,55 @@ def set_approved_updated(sender, instance, update_fields, **kwargs):
@receiver(post_save, sender=LibraryFollow)
@receiver(post_save, sender=Follow)
def update_denormalization_follow_approved(sender, instance, created, **kwargs):
from funkwhale_api.music import models as music_models
updated = getattr(instance, "_approved_updated", False)
if (created or updated) and instance.actor.is_local:
if isinstance(instance, LibraryFollow):
music_models.TrackActor.create_entries(
instance.target,
actor_ids=[instance.actor.pk],
delete_existing=not instance.approved,
)
elif isinstance(instance, Follow) and not instance.target.get_channel():
# we fetch the remote actor's libraries to make the uploads available locally
builtin_lib = federation_utils.get_or_create_builtin_actor_library(
instance.target, privacy_level="everyone"
)
followers_builtin_lib = (
federation_utils.get_or_create_builtin_actor_library(
instance.target, privacy_level="followers"
)
)
music_models.TrackActor.create_entries(
builtin_lib,
actor_ids=[instance.actor.pk],
delete_existing=not instance.approved,
)
music_models.TrackActor.create_entries(
followers_builtin_lib,
actor_ids=[instance.actor.pk],
delete_existing=not instance.approved,
)
@receiver(post_delete, sender=LibraryFollow)
@receiver(post_delete, sender=Follow)
def update_denormalization_follow_deleted(sender, instance, **kwargs):
from funkwhale_api.music import models as music_models
if instance.actor.is_local:
if isinstance(instance, LibraryFollow):
music_models.TrackActor.objects.filter(
actor=instance.actor, upload__in=instance.target.uploads.all()
).delete()
elif isinstance(instance, Follow):
builtin_lib_uploads = music_models.Upload.objects.filter(
library__actor=instance.target
)
music_models.TrackActor.objects.filter(
actor=instance.actor, upload__in=builtin_lib_uploads
).delete()
from funkwhale_api.moderation import mrf
from . import activity
......
......@@ -3,12 +3,12 @@ import uuid
from django.db.models import Q
from funkwhale_api.favorites import models as favorites_models
from funkwhale_api.history import models as history_models
from funkwhale_api.music import models as music_models
from funkwhale_api.playlists import models as playlist_models
from . import activity
from . import actors
from . import models
from . import serializers
from . import activity, actors, models, serializers
logger = logging.getLogger(__name__)
inbox = activity.InboxRouter()
......@@ -166,7 +166,7 @@ def outbox_follow(context):
def outbox_create_audio(context):
upload = context["upload"]
channel = upload.library.get_channel()
followers_target = channel.actor if channel else upload.library
followers_target = channel.actor if channel else upload.library.actor
actor = channel.actor if channel else upload.library.actor
if channel:
serializer = serializers.ChannelCreateUploadSerializer(upload)
......@@ -196,7 +196,8 @@ def inbox_create_audio(payload, context):
if is_channel:
channel = context["actor"].get_channel()
serializer = serializers.ChannelCreateUploadSerializer(
data=payload, context={"channel": channel},
data=payload,
context={"channel": channel},
)
else:
serializer = serializers.UploadSerializer(
......@@ -295,7 +296,7 @@ def inbox_delete_audio(payload, context):
upload_fids = [payload["object"]["id"]]
query = Q(fid__in=upload_fids) & (
Q(library__actor=actor) | Q(track__artist__channel__actor=actor)
Q(library__actor=actor) | Q(track__artist_credit__artist__channel__actor=actor)
)
candidates = music_models.Upload.objects.filter(query)
......@@ -309,8 +310,8 @@ def outbox_delete_audio(context):
uploads = context["uploads"]
library = uploads[0].library
channel = library.get_channel()
followers_target = channel.actor if channel else library
actor = channel.actor if channel else library.actor
followers_target = channel.actor if channel else actor
serializer = serializers.ActivitySerializer(
{
"type": "Delete",
......@@ -579,7 +580,9 @@ def inbox_delete_album(payload, context):
logger.debug("Discarding deletion of empty library")
return
query = Q(fid=album_id) & (Q(attributed_to=actor) | Q(artist__channel__actor=actor))
query = Q(fid=album_id) & (
Q(attributed_to=actor) | Q(artist_credit__artist__channel__actor=actor)
)
try:
album = music_models.Album.objects.get(query)
except music_models.Album.DoesNotExist:
......@@ -592,9 +595,10 @@ def inbox_delete_album(payload, context):
@outbox.register({"type": "Delete", "object.type": "Album"})
def outbox_delete_album(context):
album = context["album"]
album_artist = album.artist_credit.all()[0].artist
actor = (
album.artist.channel.actor
if album.artist.get_channel()
album_artist.channel.actor
if album_artist.get_channel()
else album.attributed_to
)
actor = actor or actors.get_service_actor()
......@@ -610,3 +614,317 @@ def outbox_delete_album(context):
to=[activity.PUBLIC_ADDRESS, {"type": "instances_with_followers"}],
),
}
@outbox.register({"type": "Like", "object.type": "Track"})
def outbox_create_track_favorite(context):
track = context["track"]
actor = context["actor"]
serializer = serializers.ActivitySerializer(
{
"type": "Like",
"id": context["id"],
"object": {"type": "Track", "id": track.fid},
"audience": actor.user.privacy_level,
}
)
yield {
"type": "Like",
"actor": actor,
"payload": with_recipients(
serializer.data,
to=[{"type": "followers", "target": actor}],
),
}
@outbox.register({"type": "Dislike", "object.type": "Track"})
def outbox_delete_favorite(context):
favorite = context["favorite"]
actor = favorite.actor
serializer = serializers.ActivitySerializer(
{"type": "Dislike", "object": {"type": "Track", "id": favorite.track.fid}}
)
yield {
"type": "Dislike",
"actor": actor,
"payload": with_recipients(
serializer.data,
to=[{"type": "followers", "target": actor}],
),
}
@inbox.register({"type": "Like", "object.type": "Track"})
def inbox_create_favorite(payload, context):
serializer = serializers.TrackFavoriteSerializer(data=payload)
serializer.is_valid(raise_exception=True)
instance = serializer.save()
return {"object": instance}
@inbox.register({"type": "Dislike", "object.type": "Track"})
def inbox_delete_favorite(payload, context):
actor = context["actor"]
track_id = payload["object"].get("id")
query = Q(track__fid=track_id) & Q(actor=actor)
try:
favorite = favorites_models.TrackFavorite.objects.get(query)
except favorites_models.TrackFavorite.DoesNotExist:
logger.debug(
"Discarding deletion of unkwnown favorite with track : %s", track_id
)
return
favorite.delete()
@outbox.register({"type": "Listen", "object.type": "Track"})
def outbox_create_listening(context):
track = context["track"]
actor = context["actor"]
serializer = serializers.ActivitySerializer(
{
"type": "Listen",
"id": context["id"],
"object": {"type": "Track", "id": track.fid},
"audience": actor.user.privacy_level,
}
)
yield {
"type": "Listen",
"actor": actor,
"payload": with_recipients(
serializer.data,
to=[{"type": "followers", "target": actor}],
),
}
@outbox.register({"type": "Delete", "object.type": "Listen"})
def outbox_delete_listening(context):
listening = context["listening"]
actor = listening.actor
serializer = serializers.ActivitySerializer(
{"type": "Delete", "object": {"type": "Listen", "id": listening.fid}}
)
yield {
"type": "Delete",
"actor": actor,
"payload": with_recipients(
serializer.data,
to=[{"type": "followers", "target": actor}],
),
}
@inbox.register({"type": "Listen", "object.type": "Track"})
def inbox_create_listening(payload, context):
serializer = serializers.ListeningSerializer(data=payload)
serializer.is_valid(raise_exception=True)
instance = serializer.save()
return {"object": instance}
@inbox.register({"type": "Delete", "object.type": "Listen"})
def inbox_delete_listening(payload, context):
actor = context["actor"]
listening_id = payload["object"].get("id")
query = Q(fid=listening_id) & Q(actor=actor)
try:
favorite = history_models.Listening.objects.get(query)
except history_models.Listening.DoesNotExist:
logger.debug("Discarding deletion of unkwnown listening %s", listening_id)
return
favorite.delete()
@outbox.register({"type": "Update", "object.type": "Person"})
def outbox_update_actor(context):
actor = context["actor"]
# this is a bit hacky, but we want to send the privacy level of the user as audience
# to delete or fetch all user activities inheriting user.privacy_level
setattr(actor, "audience", actor.user.privacy_level)
serializer = serializers.ActivitySerializer(
{"type": "Update", "object": serializers.ActorSerializer(actor).data}
)
yield {
"type": "Update",
"actor": actor,
"payload": with_recipients(
serializer.data,
to=[activity.PUBLIC_ADDRESS, {"type": "instances_with_followers"}],
),
}
@inbox.register({"type": "Update", "object.type": "Person"})
def inbox_update_actor(payload, context):
actor = context["actor"]
serializer = serializers.ActorSerializer(data=payload["object"])
if serializer.is_valid() and "audience" in serializer.validated_data:
privacy_level = serializer.validated_data["audience"]
if privacy_level in [
"me",
"instance",
]:
# we delete all activities inheriting user.privacy_level
history_models.Listening.objects.filter(actor=actor).delete()
favorites_models.TrackFavorite.objects.filter(actor=actor).delete()
@outbox.register({"type": "Create", "object.type": "Playlist"})
def outbox_create_playlist(context):
playlist = context["playlist"]
serializer = serializers.ActivitySerializer(
{
"type": "Create",
"actor": playlist.actor,
"id": playlist.fid,
"object": serializers.PlaylistSerializer(playlist).data,
}
)
yield {
"type": "Create",
"actor": playlist.actor,
"payload": with_recipients(
serializer.data,
to=[{"type": "followers", "target": playlist.actor}],
),
}
@outbox.register({"type": "Delete", "object.type": "Playlist"})
def outbox_delete_playlist(context):
playlist = context["playlist"]
actor = playlist.actor
serializer = serializers.ActivitySerializer(
{"type": "Delete", "object": {"type": "Playlist", "id": playlist.fid}}
)
yield {
"type": "Delete",
"actor": actor,
"payload": with_recipients(
serializer.data,
to=[activity.PUBLIC_ADDRESS, {"type": "instances_with_followers"}],
),
}
@inbox.register({"type": "Create", "object.type": "Playlist"})
def inbox_create_playlist(payload, context):
serializer = serializers.PlaylistSerializer(data=payload["object"])
serializer.is_valid(raise_exception=True)
instance = serializer.save()
return {"object": instance}
@inbox.register({"type": "Delete", "object.type": "Playlist"})
def inbox_delete_playlist(payload, context):
actor = context["actor"]
playlist_id = payload["object"].get("id")
query = Q(fid=playlist_id) & Q(actor=actor)
try:
playlist = playlist_models.Playlist.objects.get(query)
except playlist_models.Playlist.DoesNotExist:
logger.debug("Discarding deletion of unkwnown listening %s", playlist_id)
return
playlist.playlist_tracks.all().delete()
playlist.delete()
@inbox.register({"type": "Update", "object.type": "Playlist"})
def inbox_update_playlist(payload, context):
"""If we receive an update on an unkwnown playlist, we create the playlist"""
playlist_id = payload["object"].get("id")
serializer = serializers.PlaylistSerializer(data=payload["object"])
if serializer.is_valid(raise_exception=True):
playlist = serializer.save()
# we update the playlist.library to get the plt.track.uploads locally
if follows := playlist.library.received_follows.filter(approved=True):
playlist.library.schedule_scan(follows[0].actor, force=True)
# we trigger a scan since we use this activity to avoid sending many PlaylistTracks activities
playlist.schedule_scan(actors.get_service_actor(), force=True)
return
else:
logger.debug(
"Discarding update of playlist_id %s because of payload errors: %s",
playlist_id,
serializer.errors,
)
@outbox.register({"type": "Update", "object.type": "Playlist"})
def outbox_update_playlist(context):
playlist = context["playlist"]
serializer = serializers.ActivitySerializer(
{"type": "Update", "object": serializers.PlaylistSerializer(playlist).data}
)
yield {
"type": "Update",
"actor": playlist.actor,
"payload": with_recipients(
serializer.data,
to=[{"type": "followers", "target": playlist.actor}],
),
}
@inbox.register({"type": "Update", "object.type": "AudioCollection"})
def inbox_update_audiocollection(payload, context):
serializer = serializers.AudioCollectionSerializer(
data=payload["object"], context=context
)
serializer.is_valid(raise_exception=True)
serializer.save()
@outbox.register({"type": "Update", "object.type": "AudioCollection"})
def outbox_update_audiocollection(context):
audios = context["audios"]
serializer = serializers.ActivitySerializer(
{"type": "Update", "object": serializers.AudioCollectionSerializer(audios).data}
)
yield {
"type": "Update",
"actor": audios[0].library.actor,
"payload": with_recipients(
serializer.data,
to=[activity.PUBLIC_ADDRESS, {"type": "instances_with_followers"}],
),
}
@inbox.register({"type": "Delete", "object.type": "AudioCollection"})
def inbox_delete_audiocollection(payload, context):
serializer = serializers.AudioCollectionSerializer(
data=payload["object"], context=context
)
serializer.is_valid(raise_exception=True)
for upload in serializer.validated_data["items"]:
music_models.Upload.objects.filter(fid=upload["id"]).delete()
@outbox.register({"type": "Delete", "object.type": "AudioCollection"})
def outbox_delete_audiocollection(context):
audios = context["audios"]
serializer = serializers.ActivitySerializer(
{"type": "Delete", "object": serializers.AudioCollectionSerializer(audios).data}
)
yield {
"type": "Delete",
"actor": audios[0].library.actor,
"payload": with_recipients(
serializer.data,
to=[activity.PUBLIC_ADDRESS, {"type": "instances_with_followers"}],
),
}