import uuid from django.contrib.contenttypes.fields import GenericRelation from django.core.serializers.json import DjangoJSONEncoder from django.db import models from django.db.models import JSONField from django.db.models.signals import post_delete from django.dispatch import receiver from django.urls import reverse from django.utils import timezone from funkwhale_api.federation import keys from funkwhale_api.federation import models as federation_models from funkwhale_api.federation import utils as federation_utils from funkwhale_api.users import models as user_models def empty_dict(): return {} class ChannelQuerySet(models.QuerySet): def external_rss(self, include=True): from funkwhale_api.federation import actors query = models.Q( attributed_to=actors.get_service_actor(), actor__preferred_username__startswith="rssfeed-", ) if include: return self.filter(query) return self.exclude(query) def subscribed(self, actor): if not actor: return self.none() subscriptions = actor.emitted_follows.filter( approved=True, target__channel__isnull=False ) return self.filter(actor__in=subscriptions.values_list("target", flat=True)) class Channel(models.Model): uuid = models.UUIDField(default=uuid.uuid4, unique=True) artist = models.OneToOneField( "music.Artist", on_delete=models.CASCADE, related_name="channel" ) # the owner of the channel attributed_to = models.ForeignKey( "federation.Actor", on_delete=models.CASCADE, related_name="owned_channels" ) # the federation actor created for the channel # (the one people can follow to receive updates) actor = models.OneToOneField( "federation.Actor", on_delete=models.CASCADE, related_name="channel" ) library = models.OneToOneField( "music.Library", on_delete=models.CASCADE, related_name="channel" ) creation_date = models.DateTimeField(default=timezone.now) rss_url = models.URLField(max_length=500, null=True, blank=True) # metadata to enhance rss feed metadata = JSONField( default=empty_dict, max_length=50000, encoder=DjangoJSONEncoder, blank=True ) fetches = GenericRelation( "federation.Fetch", content_type_field="object_content_type", object_id_field="object_id", ) objects = ChannelQuerySet.as_manager() @property def fid(self): if not self.is_external_rss: return self.actor.fid @property def is_local(self) -> bool: return self.actor.is_local @property def is_external_rss(self): return self.actor.preferred_username.startswith("rssfeed-") def get_absolute_url(self): suffix = self.uuid if self.actor.is_local: suffix = self.actor.preferred_username else: suffix = self.actor.full_username return federation_utils.full_url(f"/channels/{suffix}") def get_rss_url(self): if not self.artist.is_local or self.is_external_rss: return self.rss_url return federation_utils.full_url( reverse( "api:v1:channels-rss", kwargs={"composite": self.actor.preferred_username}, ) ) def generate_actor(username, **kwargs): actor_data = user_models.get_actor_data(username, **kwargs) private, public = keys.get_key_pair() actor_data["private_key"] = private.decode("utf-8") actor_data["public_key"] = public.decode("utf-8") return federation_models.Actor.objects.create(**actor_data) @receiver(post_delete, sender=Channel) def delete_channel_related_objs(instance, **kwargs): instance.library.delete() instance.artist.delete()