serializers.py 27.2 KB
Newer Older
1
import logging
2
3
import urllib.parse

4
from django.core.paginator import Paginator
5
from django.db import transaction
6

7
from rest_framework import serializers
8

9
from funkwhale_api.common import utils as funkwhale_utils
10
11
12
from funkwhale_api.common import serializers as common_serializers
from funkwhale_api.music import models as music_models
from funkwhale_api.music import tasks as music_tasks
13
from . import activity
14
from . import filters
15
from . import models
16
17
18
from . import utils


Eliot Berriot's avatar
Eliot Berriot committed
19
AP_CONTEXT = [
Eliot Berriot's avatar
Eliot Berriot committed
20
21
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1",
Eliot Berriot's avatar
Eliot Berriot committed
22
23
24
    {},
]

25
26
logger = logging.getLogger(__name__)

27

28
class ActorSerializer(serializers.Serializer):
29
30
31
    id = serializers.URLField(max_length=500)
    outbox = serializers.URLField(max_length=500)
    inbox = serializers.URLField(max_length=500)
32
33
34
35
    type = serializers.ChoiceField(choices=models.TYPE_CHOICES)
    preferredUsername = serializers.CharField()
    manuallyApprovesFollowers = serializers.NullBooleanField(required=False)
    name = serializers.CharField(required=False, max_length=200)
Eliot Berriot's avatar
Eliot Berriot committed
36
    summary = serializers.CharField(max_length=None, required=False)
37
38
    followers = serializers.URLField(max_length=500, required=False, allow_null=True)
    following = serializers.URLField(max_length=500, required=False, allow_null=True)
39
    publicKey = serializers.JSONField(required=False)
40

41
    def to_representation(self, instance):
42
        ret = {
Eliot Berriot's avatar
Eliot Berriot committed
43
44
45
46
47
            "id": instance.url,
            "outbox": instance.outbox_url,
            "inbox": instance.inbox_url,
            "preferredUsername": instance.preferred_username,
            "type": instance.type,
48
49
        }
        if instance.name:
Eliot Berriot's avatar
Eliot Berriot committed
50
            ret["name"] = instance.name
51
        if instance.followers_url:
Eliot Berriot's avatar
Eliot Berriot committed
52
            ret["followers"] = instance.followers_url
53
        if instance.following_url:
Eliot Berriot's avatar
Eliot Berriot committed
54
            ret["following"] = instance.following_url
55
        if instance.summary:
Eliot Berriot's avatar
Eliot Berriot committed
56
            ret["summary"] = instance.summary
57
        if instance.manually_approves_followers is not None:
Eliot Berriot's avatar
Eliot Berriot committed
58
            ret["manuallyApprovesFollowers"] = instance.manually_approves_followers
59

Eliot Berriot's avatar
Eliot Berriot committed
60
        ret["@context"] = AP_CONTEXT
61
        if instance.public_key:
Eliot Berriot's avatar
Eliot Berriot committed
62
63
64
65
            ret["publicKey"] = {
                "owner": instance.url,
                "publicKeyPem": instance.public_key,
                "id": "{}#main-key".format(instance.url),
66
            }
Eliot Berriot's avatar
Eliot Berriot committed
67
        ret["endpoints"] = {}
68
        if instance.shared_inbox_url:
Eliot Berriot's avatar
Eliot Berriot committed
69
            ret["endpoints"]["sharedInbox"] = instance.shared_inbox_url
70
71
72
        return ret

    def prepare_missing_fields(self):
73
        kwargs = {
Eliot Berriot's avatar
Eliot Berriot committed
74
75
76
77
78
79
80
81
82
            "url": self.validated_data["id"],
            "outbox_url": self.validated_data["outbox"],
            "inbox_url": self.validated_data["inbox"],
            "following_url": self.validated_data.get("following"),
            "followers_url": self.validated_data.get("followers"),
            "summary": self.validated_data.get("summary"),
            "type": self.validated_data["type"],
            "name": self.validated_data.get("name"),
            "preferred_username": self.validated_data["preferredUsername"],
83
        }
Eliot Berriot's avatar
Eliot Berriot committed
84
        maf = self.validated_data.get("manuallyApprovesFollowers")
85
        if maf is not None:
Eliot Berriot's avatar
Eliot Berriot committed
86
87
88
89
90
91
            kwargs["manually_approves_followers"] = maf
        domain = urllib.parse.urlparse(kwargs["url"]).netloc
        kwargs["domain"] = domain
        for endpoint, url in self.initial_data.get("endpoints", {}).items():
            if endpoint == "sharedInbox":
                kwargs["shared_inbox_url"] = url
92
93
                break
        try:
Eliot Berriot's avatar
Eliot Berriot committed
94
            kwargs["public_key"] = self.initial_data["publicKey"]["publicKeyPem"]
95
96
97
98
99
        except KeyError:
            pass
        return kwargs

    def build(self):
100
101
        d = self.prepare_missing_fields()
        return models.Actor(**d)
102
103

    def save(self, **kwargs):
104
105
        d = self.prepare_missing_fields()
        d.update(kwargs)
Eliot Berriot's avatar
Eliot Berriot committed
106
        return models.Actor.objects.update_or_create(url=d["url"], defaults=d)[0]
107

Eliot Berriot's avatar
Eliot Berriot committed
108
109
110
111
112
    def validate_summary(self, value):
        if value:
            return value[:500]


113
114
115
116
class APIActorSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Actor
        fields = [
Eliot Berriot's avatar
Eliot Berriot committed
117
118
119
120
121
122
123
124
125
126
            "id",
            "url",
            "creation_date",
            "summary",
            "preferred_username",
            "name",
            "last_fetch_date",
            "domain",
            "type",
            "manually_approves_followers",
127
        ]
128
129


130
class LibraryActorSerializer(ActorSerializer):
Eliot Berriot's avatar
Eliot Berriot committed
131
    url = serializers.ListField(child=serializers.JSONField())
132
133
134

    def validate(self, validated_data):
        try:
Eliot Berriot's avatar
Eliot Berriot committed
135
            urls = validated_data["url"]
136
        except KeyError:
Eliot Berriot's avatar
Eliot Berriot committed
137
            raise serializers.ValidationError("Missing URL field")
138
139
140

        for u in urls:
            try:
Eliot Berriot's avatar
Eliot Berriot committed
141
                if u["name"] != "library":
142
                    continue
Eliot Berriot's avatar
Eliot Berriot committed
143
                validated_data["library_url"] = u["href"]
144
145
146
147
148
149
150
                break
            except KeyError:
                continue

        return validated_data


151
152
153
154
class APIFollowSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Follow
        fields = [
Eliot Berriot's avatar
Eliot Berriot committed
155
156
157
158
159
160
            "uuid",
            "actor",
            "target",
            "approved",
            "creation_date",
            "modification_date",
161
162
163
        ]


Eliot Berriot's avatar
Eliot Berriot committed
164
class APILibrarySerializer(serializers.ModelSerializer):
165
166
    actor = APIActorSerializer()
    follow = APIFollowSerializer()
Eliot Berriot's avatar
Eliot Berriot committed
167
168
169

    class Meta:
        model = models.Library
170
171

        read_only_fields = [
Eliot Berriot's avatar
Eliot Berriot committed
172
173
174
175
176
177
178
179
            "actor",
            "uuid",
            "url",
            "tracks_count",
            "follow",
            "fetched_date",
            "modification_date",
            "creation_date",
Eliot Berriot's avatar
Eliot Berriot committed
180
        ]
181
        fields = [
Eliot Berriot's avatar
Eliot Berriot committed
182
183
184
            "autoimport",
            "federation_enabled",
            "download_files",
185
        ] + read_only_fields
Eliot Berriot's avatar
Eliot Berriot committed
186
187


188
189
190
191
class APILibraryScanSerializer(serializers.Serializer):
    until = serializers.DateTimeField(required=False)


192
193
194
195
196
197
class APILibraryFollowUpdateSerializer(serializers.Serializer):
    follow = serializers.IntegerField()
    approved = serializers.BooleanField()

    def validate_follow(self, value):
        from . import actors
Eliot Berriot's avatar
Eliot Berriot committed
198
199
200

        library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
        qs = models.Follow.objects.filter(pk=value, target=library_actor)
201
202
203
        try:
            return qs.get()
        except models.Follow.DoesNotExist:
Eliot Berriot's avatar
Eliot Berriot committed
204
            raise serializers.ValidationError("Invalid follow")
205
206

    def save(self):
Eliot Berriot's avatar
Eliot Berriot committed
207
208
        new_status = self.validated_data["approved"]
        follow = self.validated_data["follow"]
209
210
211
212
        if new_status == follow.approved:
            return follow

        follow.approved = new_status
Eliot Berriot's avatar
Eliot Berriot committed
213
        follow.save(update_fields=["approved", "modification_date"])
214
215
216
217
218
        if new_status:
            activity.accept_follow(follow)
        return follow


219
class APILibraryCreateSerializer(serializers.ModelSerializer):
220
    actor = serializers.URLField(max_length=500)
221
    federation_enabled = serializers.BooleanField()
222
    uuid = serializers.UUIDField(read_only=True)
223
224
225

    class Meta:
        model = models.Library
Eliot Berriot's avatar
Eliot Berriot committed
226
        fields = ["uuid", "actor", "autoimport", "federation_enabled", "download_files"]
227
228
229
230
231

    def validate(self, validated_data):
        from . import actors
        from . import library

Eliot Berriot's avatar
Eliot Berriot committed
232
        actor_url = validated_data["actor"]
233
234
235
236
237
238
239
        actor_data = actors.get_actor_data(actor_url)
        acs = LibraryActorSerializer(data=actor_data)
        acs.is_valid(raise_exception=True)
        try:
            actor = models.Actor.objects.get(url=actor_url)
        except models.Actor.DoesNotExist:
            actor = acs.save()
Eliot Berriot's avatar
Eliot Berriot committed
240
241
242
        library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
        validated_data["follow"] = models.Follow.objects.get_or_create(
            actor=library_actor, target=actor
243
        )[0]
Eliot Berriot's avatar
Eliot Berriot committed
244
        if validated_data["follow"].approved is None:
245
246
            funkwhale_utils.on_commit(
                activity.deliver,
Eliot Berriot's avatar
Eliot Berriot committed
247
248
249
                FollowSerializer(validated_data["follow"]).data,
                on_behalf_of=validated_data["follow"].actor,
                to=[validated_data["follow"].target.url],
250
251
            )

Eliot Berriot's avatar
Eliot Berriot committed
252
253
        library_data = library.get_library_data(acs.validated_data["library_url"])
        if "errors" in library_data:
254
255
256
            # we pass silently because it may means we require permission
            # before scanning
            pass
Eliot Berriot's avatar
Eliot Berriot committed
257
258
259
        validated_data["library"] = library_data
        validated_data["library"].setdefault("id", acs.validated_data["library_url"])
        validated_data["actor"] = actor
260
261
262
        return validated_data

    def create(self, validated_data):
Eliot Berriot's avatar
Eliot Berriot committed
263
        library = models.Library.objects.update_or_create(
Eliot Berriot's avatar
Eliot Berriot committed
264
            url=validated_data["library"]["id"],
265
            defaults={
Eliot Berriot's avatar
Eliot Berriot committed
266
267
268
269
270
271
272
                "actor": validated_data["actor"],
                "follow": validated_data["follow"],
                "tracks_count": validated_data["library"].get("totalItems"),
                "federation_enabled": validated_data["federation_enabled"],
                "autoimport": validated_data["autoimport"],
                "download_files": validated_data["download_files"],
            },
273
274
275
276
        )[0]
        return library


277
278
class APILibraryTrackSerializer(serializers.ModelSerializer):
    library = APILibrarySerializer()
279
    status = serializers.SerializerMethodField()
280
281
282
283

    class Meta:
        model = models.LibraryTrack
        fields = [
Eliot Berriot's avatar
Eliot Berriot committed
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
            "id",
            "url",
            "audio_url",
            "audio_mimetype",
            "creation_date",
            "modification_date",
            "fetched_date",
            "published_date",
            "metadata",
            "artist_name",
            "album_title",
            "title",
            "library",
            "local_track_file",
            "status",
299
300
        ]

301
302
303
    def get_status(self, o):
        try:
            if o.local_track_file is not None:
Eliot Berriot's avatar
Eliot Berriot committed
304
                return "imported"
305
306
307
        except music_models.TrackFile.DoesNotExist:
            pass
        for job in o.import_jobs.all():
Eliot Berriot's avatar
Eliot Berriot committed
308
309
310
            if job.status == "pending":
                return "import_pending"
        return "not_imported"
311

312

313
class FollowSerializer(serializers.Serializer):
314
315
316
    id = serializers.URLField(max_length=500)
    object = serializers.URLField(max_length=500)
    actor = serializers.URLField(max_length=500)
Eliot Berriot's avatar
Eliot Berriot committed
317
    type = serializers.ChoiceField(choices=["Follow"])
Eliot Berriot's avatar
Eliot Berriot committed
318

319
    def validate_object(self, v):
Eliot Berriot's avatar
Eliot Berriot committed
320
        expected = self.context.get("follow_target")
321
        if expected and expected.url != v:
Eliot Berriot's avatar
Eliot Berriot committed
322
            raise serializers.ValidationError("Invalid target")
323
324
325
        try:
            return models.Actor.objects.get(url=v)
        except models.Actor.DoesNotExist:
Eliot Berriot's avatar
Eliot Berriot committed
326
            raise serializers.ValidationError("Target not found")
327
328

    def validate_actor(self, v):
Eliot Berriot's avatar
Eliot Berriot committed
329
        expected = self.context.get("follow_actor")
330
        if expected and expected.url != v:
Eliot Berriot's avatar
Eliot Berriot committed
331
            raise serializers.ValidationError("Invalid actor")
332
333
334
        try:
            return models.Actor.objects.get(url=v)
        except models.Actor.DoesNotExist:
Eliot Berriot's avatar
Eliot Berriot committed
335
            raise serializers.ValidationError("Actor not found")
336
337
338

    def save(self, **kwargs):
        return models.Follow.objects.get_or_create(
Eliot Berriot's avatar
Eliot Berriot committed
339
340
            actor=self.validated_data["actor"],
            target=self.validated_data["object"],
341
342
            **kwargs,
        )[0]
Eliot Berriot's avatar
Eliot Berriot committed
343
344

    def to_representation(self, instance):
345
        return {
Eliot Berriot's avatar
Eliot Berriot committed
346
347
348
349
350
            "@context": AP_CONTEXT,
            "actor": instance.actor.url,
            "id": instance.get_federation_url(),
            "object": instance.target.url,
            "type": "Follow",
351
        }
Eliot Berriot's avatar
Eliot Berriot committed
352
353
354
        return ret


355
class APIFollowSerializer(serializers.ModelSerializer):
356
357
358
    actor = APIActorSerializer()
    target = APIActorSerializer()

359
360
361
    class Meta:
        model = models.Follow
        fields = [
Eliot Berriot's avatar
Eliot Berriot committed
362
363
364
365
366
367
368
            "uuid",
            "id",
            "approved",
            "creation_date",
            "modification_date",
            "actor",
            "target",
369
370
371
        ]


372
class AcceptFollowSerializer(serializers.Serializer):
373
374
    id = serializers.URLField(max_length=500)
    actor = serializers.URLField(max_length=500)
375
    object = FollowSerializer()
Eliot Berriot's avatar
Eliot Berriot committed
376
    type = serializers.ChoiceField(choices=["Accept"])
377
378

    def validate_actor(self, v):
Eliot Berriot's avatar
Eliot Berriot committed
379
        expected = self.context.get("follow_target")
380
        if expected and expected.url != v:
Eliot Berriot's avatar
Eliot Berriot committed
381
            raise serializers.ValidationError("Invalid actor")
382
383
384
        try:
            return models.Actor.objects.get(url=v)
        except models.Actor.DoesNotExist:
Eliot Berriot's avatar
Eliot Berriot committed
385
            raise serializers.ValidationError("Actor not found")
386
387
388

    def validate(self, validated_data):
        # we ensure the accept actor actually match the follow target
Eliot Berriot's avatar
Eliot Berriot committed
389
390
        if validated_data["actor"] != validated_data["object"]["object"]:
            raise serializers.ValidationError("Actor mismatch")
391
        try:
Eliot Berriot's avatar
Eliot Berriot committed
392
393
394
395
396
397
398
399
            validated_data["follow"] = (
                models.Follow.objects.filter(
                    target=validated_data["actor"],
                    actor=validated_data["object"]["actor"],
                )
                .exclude(approved=True)
                .get()
            )
400
        except models.Follow.DoesNotExist:
Eliot Berriot's avatar
Eliot Berriot committed
401
            raise serializers.ValidationError("No follow to accept")
402
403
404
405
406
        return validated_data

    def to_representation(self, instance):
        return {
            "@context": AP_CONTEXT,
Eliot Berriot's avatar
Eliot Berriot committed
407
            "id": instance.get_federation_url() + "/accept",
408
409
            "type": "Accept",
            "actor": instance.target.url,
Eliot Berriot's avatar
Eliot Berriot committed
410
            "object": FollowSerializer(instance).data,
411
412
413
        }

    def save(self):
Eliot Berriot's avatar
Eliot Berriot committed
414
415
416
        self.validated_data["follow"].approved = True
        self.validated_data["follow"].save()
        return self.validated_data["follow"]
417
418
419


class UndoFollowSerializer(serializers.Serializer):
420
421
    id = serializers.URLField(max_length=500)
    actor = serializers.URLField(max_length=500)
422
    object = FollowSerializer()
Eliot Berriot's avatar
Eliot Berriot committed
423
    type = serializers.ChoiceField(choices=["Undo"])
424
425

    def validate_actor(self, v):
Eliot Berriot's avatar
Eliot Berriot committed
426
        expected = self.context.get("follow_target")
427
        if expected and expected.url != v:
Eliot Berriot's avatar
Eliot Berriot committed
428
            raise serializers.ValidationError("Invalid actor")
429
430
431
        try:
            return models.Actor.objects.get(url=v)
        except models.Actor.DoesNotExist:
Eliot Berriot's avatar
Eliot Berriot committed
432
            raise serializers.ValidationError("Actor not found")
433
434
435

    def validate(self, validated_data):
        # we ensure the accept actor actually match the follow actor
Eliot Berriot's avatar
Eliot Berriot committed
436
437
        if validated_data["actor"] != validated_data["object"]["actor"]:
            raise serializers.ValidationError("Actor mismatch")
438
        try:
Eliot Berriot's avatar
Eliot Berriot committed
439
440
            validated_data["follow"] = models.Follow.objects.filter(
                actor=validated_data["actor"], target=validated_data["object"]["object"]
441
442
            ).get()
        except models.Follow.DoesNotExist:
Eliot Berriot's avatar
Eliot Berriot committed
443
            raise serializers.ValidationError("No follow to remove")
444
445
446
447
448
        return validated_data

    def to_representation(self, instance):
        return {
            "@context": AP_CONTEXT,
Eliot Berriot's avatar
Eliot Berriot committed
449
            "id": instance.get_federation_url() + "/undo",
450
451
            "type": "Undo",
            "actor": instance.actor.url,
Eliot Berriot's avatar
Eliot Berriot committed
452
            "object": FollowSerializer(instance).data,
453
454
455
        }

    def save(self):
Eliot Berriot's avatar
Eliot Berriot committed
456
        return self.validated_data["follow"].delete()
457
458


459
460
class ActorWebfingerSerializer(serializers.Serializer):
    subject = serializers.CharField()
461
    aliases = serializers.ListField(child=serializers.URLField(max_length=500))
462
    links = serializers.ListField()
463
    actor_url = serializers.URLField(max_length=500, required=False)
464
465

    def validate(self, validated_data):
Eliot Berriot's avatar
Eliot Berriot committed
466
467
        validated_data["actor_url"] = None
        for l in validated_data["links"]:
468
            try:
Eliot Berriot's avatar
Eliot Berriot committed
469
                if not l["rel"] == "self":
470
                    continue
Eliot Berriot's avatar
Eliot Berriot committed
471
                if not l["type"] == "application/activity+json":
472
                    continue
Eliot Berriot's avatar
Eliot Berriot committed
473
                validated_data["actor_url"] = l["href"]
474
475
476
                break
            except KeyError:
                pass
Eliot Berriot's avatar
Eliot Berriot committed
477
478
        if validated_data["actor_url"] is None:
            raise serializers.ValidationError("No valid actor url found")
479
        return validated_data
480
481
482

    def to_representation(self, instance):
        data = {}
Eliot Berriot's avatar
Eliot Berriot committed
483
484
485
        data["subject"] = "acct:{}".format(instance.webfinger_subject)
        data["links"] = [
            {"rel": "self", "href": instance.url, "type": "application/activity+json"}
486
        ]
Eliot Berriot's avatar
Eliot Berriot committed
487
        data["aliases"] = [instance.url]
488
        return data
489
490
491


class ActivitySerializer(serializers.Serializer):
492
493
    actor = serializers.URLField(max_length=500)
    id = serializers.URLField(max_length=500, required=False)
Eliot Berriot's avatar
Eliot Berriot committed
494
    type = serializers.ChoiceField(choices=[(c, c) for c in activity.ACTIVITY_TYPES])
495
496
497
498
    object = serializers.JSONField()

    def validate_object(self, value):
        try:
Eliot Berriot's avatar
Eliot Berriot committed
499
            type = value["type"]
500
        except KeyError:
Eliot Berriot's avatar
Eliot Berriot committed
501
            raise serializers.ValidationError("Missing object type")
Eliot Berriot's avatar
Eliot Berriot committed
502
503
504
        except TypeError:
            # probably a URL
            return value
505
506
507
        try:
            object_serializer = OBJECT_SERIALIZERS[type]
        except KeyError:
Eliot Berriot's avatar
Eliot Berriot committed
508
            raise serializers.ValidationError("Unsupported type {}".format(type))
509
510
511
512
513
514

        serializer = object_serializer(data=value)
        serializer.is_valid(raise_exception=True)
        return serializer.data

    def validate_actor(self, value):
Eliot Berriot's avatar
Eliot Berriot committed
515
        request_actor = self.context.get("actor")
516
517
        if request_actor and request_actor.url != value:
            raise serializers.ValidationError(
Eliot Berriot's avatar
Eliot Berriot committed
518
                "The actor making the request do not match" " the activity actor"
519
520
521
            )
        return value

522
523
524
525
    def to_representation(self, conf):
        d = {}
        d.update(conf)

Eliot Berriot's avatar
Eliot Berriot committed
526
527
        if self.context.get("include_ap_context", True):
            d["@context"] = AP_CONTEXT
528
529
        return d

530
531

class ObjectSerializer(serializers.Serializer):
532
533
    id = serializers.URLField(max_length=500)
    url = serializers.URLField(max_length=500, required=False, allow_null=True)
Eliot Berriot's avatar
Eliot Berriot committed
534
535
536
537
538
539
    type = serializers.ChoiceField(choices=[(c, c) for c in activity.OBJECT_TYPES])
    content = serializers.CharField(required=False, allow_null=True)
    summary = serializers.CharField(required=False, allow_null=True)
    name = serializers.CharField(required=False, allow_null=True)
    published = serializers.DateTimeField(required=False, allow_null=True)
    updated = serializers.DateTimeField(required=False, allow_null=True)
540
    to = serializers.ListField(
Eliot Berriot's avatar
Eliot Berriot committed
541
542
        child=serializers.URLField(max_length=500), required=False, allow_null=True
    )
543
    cc = serializers.ListField(
Eliot Berriot's avatar
Eliot Berriot committed
544
545
        child=serializers.URLField(max_length=500), required=False, allow_null=True
    )
546
    bto = serializers.ListField(
Eliot Berriot's avatar
Eliot Berriot committed
547
548
        child=serializers.URLField(max_length=500), required=False, allow_null=True
    )
549
    bcc = serializers.ListField(
Eliot Berriot's avatar
Eliot Berriot committed
550
551
552
        child=serializers.URLField(max_length=500), required=False, allow_null=True
    )

553

Eliot Berriot's avatar
Eliot Berriot committed
554
OBJECT_SERIALIZERS = {t: ObjectSerializer for t in activity.OBJECT_TYPES}
555
556
557


class PaginatedCollectionSerializer(serializers.Serializer):
Eliot Berriot's avatar
Eliot Berriot committed
558
    type = serializers.ChoiceField(choices=["Collection"])
559
    totalItems = serializers.IntegerField(min_value=0)
560
561
562
563
    actor = serializers.URLField(max_length=500)
    id = serializers.URLField(max_length=500)
    first = serializers.URLField(max_length=500)
    last = serializers.URLField(max_length=500)
564
565

    def to_representation(self, conf):
Eliot Berriot's avatar
Eliot Berriot committed
566
567
        paginator = Paginator(conf["items"], conf.get("page_size", 20))
        first = funkwhale_utils.set_query_parameter(conf["id"], page=1)
568
        current = first
Eliot Berriot's avatar
Eliot Berriot committed
569
        last = funkwhale_utils.set_query_parameter(conf["id"], page=paginator.num_pages)
570
        d = {
Eliot Berriot's avatar
Eliot Berriot committed
571
572
573
574
575
576
577
            "id": conf["id"],
            "actor": conf["actor"].url,
            "totalItems": paginator.count,
            "type": "Collection",
            "current": current,
            "first": first,
            "last": last,
578
        }
Eliot Berriot's avatar
Eliot Berriot committed
579
580
        if self.context.get("include_ap_context", True):
            d["@context"] = AP_CONTEXT
581
582
583
584
        return d


class CollectionPageSerializer(serializers.Serializer):
Eliot Berriot's avatar
Eliot Berriot committed
585
    type = serializers.ChoiceField(choices=["CollectionPage"])
586
587
    totalItems = serializers.IntegerField(min_value=0)
    items = serializers.ListField()
588
589
590
591
592
593
594
    actor = serializers.URLField(max_length=500)
    id = serializers.URLField(max_length=500)
    first = serializers.URLField(max_length=500)
    last = serializers.URLField(max_length=500)
    next = serializers.URLField(max_length=500, required=False)
    prev = serializers.URLField(max_length=500, required=False)
    partOf = serializers.URLField(max_length=500)
595

596
    def validate_items(self, v):
Eliot Berriot's avatar
Eliot Berriot committed
597
        item_serializer = self.context.get("item_serializer")
598
599
600
        if not item_serializer:
            return v
        raw_items = [item_serializer(data=i, context=self.context) for i in v]
601
        valid_items = []
602
        for i in raw_items:
603
604
            if i.is_valid():
                valid_items.append(i)
605
            else:
Eliot Berriot's avatar
Eliot Berriot committed
606
                logger.debug("Invalid item %s: %s", i.data, i.errors)
607

608
        return valid_items
609

610
    def to_representation(self, conf):
Eliot Berriot's avatar
Eliot Berriot committed
611
612
        page = conf["page"]
        first = funkwhale_utils.set_query_parameter(conf["id"], page=1)
613
        last = funkwhale_utils.set_query_parameter(
Eliot Berriot's avatar
Eliot Berriot committed
614
615
616
            conf["id"], page=page.paginator.num_pages
        )
        id = funkwhale_utils.set_query_parameter(conf["id"], page=page.number)
617
        d = {
Eliot Berriot's avatar
Eliot Berriot committed
618
619
620
621
622
623
624
625
626
627
            "id": id,
            "partOf": conf["id"],
            "actor": conf["actor"].url,
            "totalItems": page.paginator.count,
            "type": "CollectionPage",
            "first": first,
            "last": last,
            "items": [
                conf["item_serializer"](
                    i, context={"actor": conf["actor"], "include_ap_context": False}
628
629
                ).data
                for i in page.object_list
Eliot Berriot's avatar
Eliot Berriot committed
630
            ],
631
632
633
        }

        if page.has_previous():
Eliot Berriot's avatar
Eliot Berriot committed
634
635
636
            d["prev"] = funkwhale_utils.set_query_parameter(
                conf["id"], page=page.previous_page_number()
            )
637

Eliot Berriot's avatar
Eliot Berriot committed
638
        if page.has_next():
Eliot Berriot's avatar
Eliot Berriot committed
639
640
641
            d["next"] = funkwhale_utils.set_query_parameter(
                conf["id"], page=page.next_page_number()
            )
642

Eliot Berriot's avatar
Eliot Berriot committed
643
644
        if self.context.get("include_ap_context", True):
            d["@context"] = AP_CONTEXT
645
        return d
646
647
648


class ArtistMetadataSerializer(serializers.Serializer):
649
    musicbrainz_id = serializers.UUIDField(required=False, allow_null=True)
650
651
652
653
    name = serializers.CharField()


class ReleaseMetadataSerializer(serializers.Serializer):
654
    musicbrainz_id = serializers.UUIDField(required=False, allow_null=True)
655
656
657
658
    title = serializers.CharField()


class RecordingMetadataSerializer(serializers.Serializer):
659
    musicbrainz_id = serializers.UUIDField(required=False, allow_null=True)
660
661
662
663
664
665
666
    title = serializers.CharField()


class AudioMetadataSerializer(serializers.Serializer):
    artist = ArtistMetadataSerializer()
    release = ReleaseMetadataSerializer()
    recording = RecordingMetadataSerializer()
Eliot Berriot's avatar
Eliot Berriot committed
667
668
669
    bitrate = serializers.IntegerField(required=False, allow_null=True, min_value=0)
    size = serializers.IntegerField(required=False, allow_null=True, min_value=0)
    length = serializers.IntegerField(required=False, allow_null=True, min_value=0)
670
671
672
673


class AudioSerializer(serializers.Serializer):
    type = serializers.CharField()
674
    id = serializers.URLField(max_length=500)
675
676
677
678
679
680
    url = serializers.JSONField()
    published = serializers.DateTimeField()
    updated = serializers.DateTimeField(required=False)
    metadata = AudioMetadataSerializer()

    def validate_type(self, v):
Eliot Berriot's avatar
Eliot Berriot committed
681
682
        if v != "Audio":
            raise serializers.ValidationError("Invalid type for audio")
683
684
685
686
        return v

    def validate_url(self, v):
        try:
687
            v["href"]
688
        except (KeyError, TypeError):
Eliot Berriot's avatar
Eliot Berriot committed
689
            raise serializers.ValidationError("Missing href")
690
691

        try:
Eliot Berriot's avatar
Eliot Berriot committed
692
            media_type = v["mediaType"]
693
        except (KeyError, TypeError):
Eliot Berriot's avatar
Eliot Berriot committed
694
            raise serializers.ValidationError("Missing mediaType")
695

Eliot Berriot's avatar
Eliot Berriot committed
696
697
        if not media_type or not media_type.startswith("audio/"):
            raise serializers.ValidationError("Invalid mediaType")
698
699
700
701
702

        return v

    def create(self, validated_data):
        defaults = {
Eliot Berriot's avatar
Eliot Berriot committed
703
704
705
706
707
708
709
710
            "audio_mimetype": validated_data["url"]["mediaType"],
            "audio_url": validated_data["url"]["href"],
            "metadata": validated_data["metadata"],
            "artist_name": validated_data["metadata"]["artist"]["name"],
            "album_title": validated_data["metadata"]["release"]["title"],
            "title": validated_data["metadata"]["recording"]["title"],
            "published_date": validated_data["published"],
            "modification_date": validated_data.get("updated"),
711
712
        }
        return models.LibraryTrack.objects.get_or_create(
Eliot Berriot's avatar
Eliot Berriot committed
713
            library=self.context["library"], url=validated_data["id"], defaults=defaults
714
715
716
717
718
719
720
721
        )[0]

    def to_representation(self, instance):
        track = instance.track
        album = instance.track.album
        artist = instance.track.artist

        d = {
Eliot Berriot's avatar
Eliot Berriot committed
722
723
724
725
726
727
728
729
730
            "type": "Audio",
            "id": instance.get_federation_url(),
            "name": instance.track.full_name,
            "published": instance.creation_date.isoformat(),
            "updated": instance.modification_date.isoformat(),
            "metadata": {
                "artist": {
                    "musicbrainz_id": str(artist.mbid) if artist.mbid else None,
                    "name": artist.name,
731
                },
Eliot Berriot's avatar
Eliot Berriot committed
732
733
734
                "release": {
                    "musicbrainz_id": str(album.mbid) if album.mbid else None,
                    "title": album.title,
735
                },
Eliot Berriot's avatar
Eliot Berriot committed
736
737
738
                "recording": {
                    "musicbrainz_id": str(track.mbid) if track.mbid else None,
                    "title": track.title,
739
                },
Eliot Berriot's avatar
Eliot Berriot committed
740
741
742
                "bitrate": instance.bitrate,
                "size": instance.size,
                "length": instance.duration,
743
            },
Eliot Berriot's avatar
Eliot Berriot committed
744
745
746
747
            "url": {
                "href": utils.full_url(instance.path),
                "type": "Link",
                "mediaType": instance.mimetype,
748
            },
Eliot Berriot's avatar
Eliot Berriot committed
749
            "attributedTo": [self.context["actor"].url],
750
        }
Eliot Berriot's avatar
Eliot Berriot committed
751
752
        if self.context.get("include_ap_context", True):
            d["@context"] = AP_CONTEXT
753
754
755
756
757
758
        return d


class CollectionSerializer(serializers.Serializer):
    def to_representation(self, conf):
        d = {
Eliot Berriot's avatar
Eliot Berriot committed
759
760
761
762
763
764
765
            "id": conf["id"],
            "actor": conf["actor"].url,
            "totalItems": len(conf["items"]),
            "type": "Collection",
            "items": [
                conf["item_serializer"](
                    i, context={"actor": conf["actor"], "include_ap_context": False}
766
                ).data
Eliot Berriot's avatar
Eliot Berriot committed
767
768
                for i in conf["items"]
            ],
769
770
        }

Eliot Berriot's avatar
Eliot Berriot committed
771
772
        if self.context.get("include_ap_context", True):
            d["@context"] = AP_CONTEXT
773
        return d
774
775
776


class LibraryTrackActionSerializer(common_serializers.ActionSerializer):
Eliot Berriot's avatar
Eliot Berriot committed
777
    actions = ["import"]
778
779
780
781
782
    filterset_class = filters.LibraryTrackFilter

    @transaction.atomic
    def handle_import(self, objects):
        batch = music_models.ImportBatch.objects.create(
Eliot Berriot's avatar
Eliot Berriot committed
783
            source="federation", submitted_by=self.context["submitted_by"]
784
        )
785
        jobs = []
786
        for lt in objects:
787
            job = music_models.ImportJob(
Eliot Berriot's avatar
Eliot Berriot committed
788
                batch=batch, library_track=lt, mbid=lt.mbid, source=lt.url
789
            )
790
791
792
793
            jobs.append(job)

        music_models.ImportJob.objects.bulk_create(jobs)
        music_tasks.import_batch_run.delay(import_batch_id=batch.pk)
794

Eliot Berriot's avatar
Eliot Berriot committed
795
        return {"batch": {"id": batch.pk}}