示例#1
0
 def __init__(self, *args, **kwargs):
     self.approval_enabled = preferences.get(
         "moderation__signup_approval_enabled")
     super().__init__(*args, **kwargs)
     if self.approval_enabled:
         customization = preferences.get(
             "moderation__signup_form_customization")
         self.fields[
             "request_fields"] = moderation_utils.get_signup_form_additional_fields_serializer(
                 customization)
示例#2
0
def get():
    share_stats = preferences.get("instance__nodeinfo_stats_enabled")
    data = {
        "version": "2.0",
        "software": {
            "name": "funkwhale",
            "version": funkwhale_api.__version__
        },
        "protocols": ["activitypub"],
        "services": {
            "inbound": [],
            "outbound": []
        },
        "openRegistrations": preferences.get("users__registration_enabled"),
        "usage": {
            "users": {
                "total": 0
            }
        },
        "metadata": {
            "private": preferences.get("instance__nodeinfo_private"),
            "shortDescription": preferences.get("instance__short_description"),
            "longDescription": preferences.get("instance__long_description"),
            "nodeName": preferences.get("instance__name"),
            "library": {
                "federationEnabled":
                preferences.get("federation__enabled"),
                "federationNeedsApproval":
                preferences.get("federation__music_needs_approval"),
                "anonymousCanListen":
                preferences.get("common__api_authentication_required"),
            },
        },
    }
    if share_stats:
        getter = memo(lambda: stats.get(), max_age=600)
        statistics = getter()
        data["usage"]["users"]["total"] = statistics["users"]
        data["metadata"]["library"]["tracks"] = {"total": statistics["tracks"]}
        data["metadata"]["library"]["artists"] = {
            "total": statistics["artists"]
        }
        data["metadata"]["library"]["albums"] = {"total": statistics["albums"]}
        data["metadata"]["library"]["music"] = {
            "hours": statistics["music_duration"]
        }

        data["metadata"]["usage"] = {
            "favorites": {
                "tracks": {
                    "total": statistics["track_favorites"]
                }
            },
            "listenings": {
                "total": statistics["listenings"]
            },
        }
    return data
示例#3
0
 def get(self, request, *args, **kwargs):
     existing_manifest = middleware.get_spa_file(
         settings.FUNKWHALE_SPA_HTML_ROOT, "manifest.json")
     parsed_manifest = json.loads(existing_manifest)
     parsed_manifest["short_name"] = settings.APP_NAME
     parsed_manifest["start_url"] = federation_utils.full_url("/")
     instance_name = preferences.get("instance__name")
     if instance_name:
         parsed_manifest["short_name"] = instance_name
         parsed_manifest["name"] = instance_name
     instance_description = preferences.get("instance__short_description")
     if instance_description:
         parsed_manifest["description"] = instance_description
     return Response(parsed_manifest, status=200)
示例#4
0
    def get_channel_outbox_response(self, request, channel):
        conf = {
            "id":
            channel.actor.outbox_url,
            "actor":
            channel.actor,
            "items":
            channel.library.uploads.for_federation().order_by(
                "-creation_date").prefetch_related("library__channel__actor",
                                                   "track__artist"),
            "item_serializer":
            serializers.ChannelCreateUploadSerializer,
        }
        page = request.GET.get("page")
        if page is None:
            serializer = serializers.ChannelOutboxSerializer(channel)
            data = serializer.data
        else:
            try:
                page_number = int(page)
            except Exception:
                return response.Response({"page": ["Invalid page number"]},
                                         status=400)
            conf["page_size"] = preferences.get(
                "federation__collection_page_size")
            p = paginator.Paginator(conf["items"], conf["page_size"])
            try:
                page = p.page(page_number)
                conf["page"] = page
                serializer = serializers.CollectionPageSerializer(conf)
                data = serializer.data
            except paginator.EmptyPage:
                return response.Response(status=404)

        return response.Response(data)
示例#5
0
def get_collection_response(conf,
                            querystring,
                            collection_serializer,
                            page_access_check=None):
    page = querystring.get("page")
    if page is None:
        data = collection_serializer.data
    else:
        if page_access_check and not page_access_check():
            raise exceptions.AuthenticationFailed(
                "You do not have access to this resource")
        try:
            page_number = int(page)
        except Exception:
            return response.Response({"page": ["Invalid page number"]},
                                     status=400)
        conf["page_size"] = preferences.get("federation__collection_page_size")
        p = paginator.Paginator(conf["items"], conf["page_size"])
        try:
            page = p.page(page_number)
            conf["page"] = page
            serializer = serializers.CollectionPageSerializer(conf)
            data = serializer.data
        except paginator.EmptyPage:
            return response.Response(status=404)

    return response.Response(data)
示例#6
0
def deliver_to_remote(delivery):

    if not preferences.get("federation__enabled"):
        # federation is disabled, we only deliver to local recipients
        return

    actor = delivery.activity.actor
    logger.info("Preparing activity delivery to %s", delivery.inbox_url)
    auth = signing.get_auth(actor.private_key, actor.private_key_id)
    try:
        response = session.get_session().post(
            auth=auth,
            json=delivery.activity.payload,
            url=delivery.inbox_url,
            headers={"Content-Type": "application/activity+json"},
        )
        logger.debug("Remote answered with %s", response.status_code)
        response.raise_for_status()
    except Exception:
        delivery.last_attempt_date = timezone.now()
        delivery.attempts = F("attempts") + 1
        delivery.save(update_fields=["last_attempt_date", "attempts"])
        raise
    else:
        delivery.last_attempt_date = timezone.now()
        delivery.attempts = F("attempts") + 1
        delivery.is_delivered = True
        delivery.save(
            update_fields=["last_attempt_date", "attempts", "is_delivered"])
示例#7
0
def should_transcode(upload, format, max_bitrate=None):
    if not preferences.get("music__transcoding_enabled"):
        return False
    format_need_transcoding = True
    bitrate_need_transcoding = True
    if format is None:
        format_need_transcoding = False
    elif format not in utils.EXTENSION_TO_MIMETYPE:
        # format should match supported formats
        format_need_transcoding = False
    elif upload.mimetype is None:
        # upload should have a mimetype, otherwise we cannot transcode
        format_need_transcoding = False
    elif upload.mimetype == utils.EXTENSION_TO_MIMETYPE[format]:
        # requested format sould be different than upload mimetype, otherwise
        # there is no need to transcode
        format_need_transcoding = False

    if max_bitrate is None:
        bitrate_need_transcoding = False
    elif not upload.bitrate:
        bitrate_need_transcoding = False
    elif upload.bitrate <= max_bitrate:
        bitrate_need_transcoding = False

    return format_need_transcoding or bitrate_need_transcoding
示例#8
0
def library_library(request, uuid, redirect_to_ap):
    queryset = models.Library.objects.filter(uuid=uuid)
    try:
        obj = queryset.get()
    except models.Library.DoesNotExist:
        return []

    if redirect_to_ap:
        raise middleware.ApiRedirect(obj.fid)

    library_url = utils.join_url(
        settings.FUNKWHALE_URL,
        utils.spa_reverse("library_library", kwargs={"uuid": obj.uuid}),
    )
    metas = [
        {"tag": "meta", "property": "og:url", "content": library_url},
        {"tag": "meta", "property": "og:type", "content": "website"},
        {"tag": "meta", "property": "og:title", "content": obj.name},
        {"tag": "meta", "property": "og:description", "content": obj.description},
    ]

    if preferences.get("federation__enabled"):
        metas.append(
            {
                "tag": "link",
                "rel": "alternate",
                "type": "application/activity+json",
                "href": obj.fid,
            }
        )

    return metas
示例#9
0
def notify_submitter_signup_request_refused(user_request):
    submitter_repr = user_request.submitter.preferred_username
    submitter_email = user_request.submitter.user.email
    if not submitter_email:
        logger.warning("User %s has no email configured", submitter_repr)
        return
    subject = "Your account request at {} was refused".format(
        settings.FUNKWHALE_HOSTNAME
    )
    body = [
        "Hi {},".format(submitter_repr),
        "",
        "You recently submitted an account request on our service. However, our "
        "moderation team has refused it, and as a result, you won't be able to use "
        "the service.",
    ]

    instance_contact_email = preferences.get("instance__contact_email")
    if instance_contact_email:
        body += [
            "",
            "If you think this is a mistake, please contact our team at {}.".format(
                instance_contact_email
            ),
        ]

    mail.send_mail(
        subject,
        message="\n".join(body),
        recipient_list=[submitter_email],
        from_email=settings.DEFAULT_FROM_EMAIL,
    )
示例#10
0
def check_allow_list(payload, **kwargs):
    """
    A MRF policy that only works when the moderation__allow_list_enabled
    setting is on.

    It will extract domain names from the activity ID, actor ID and activity object ID
    and discard the activity if any of those domain names isn't on the allow list.
    """
    if not preferences.get("moderation__allow_list_enabled"):
        raise mrf.Skip("Allow-listing is disabled")

    allowed_domains = set(
        federation_models.Domain.objects.filter(allowed=True).values_list(
            "name", flat=True))

    relevant_ids = [
        payload.get("actor"),
        kwargs.get("sender_id", payload.get("id")),
        utils.recursive_getattr(payload, "object.id", permissive=True),
    ]

    relevant_domains = set([
        domain for domain in
        [urllib.parse.urlparse(i).hostname for i in relevant_ids if i]
        if domain
    ])

    if relevant_domains - allowed_domains:

        raise mrf.Discard("These domains are not allowed: {}".format(
            ", ".join(relevant_domains - allowed_domains)))
示例#11
0
 def get_queryset(self):
     qs = super().get_queryset()
     qs = qs.exclude(instance_policy__is_active=True,
                     instance_policy__block_all=True)
     if preferences.get("moderation__allow_list_enabled"):
         qs = qs.filter(allowed=True)
     return qs
示例#12
0
def actor_detail_username(request, username, redirect_to_ap):
    validator = federation_utils.get_actor_data_from_username
    try:
        username_data = validator(username)
    except serializers.ValidationError:
        return []

    queryset = (models.Actor.objects.filter(
        preferred_username__iexact=username_data["username"]).local().
                select_related("attachment_icon"))
    try:
        obj = queryset.get()
    except models.Actor.DoesNotExist:
        return []

    if redirect_to_ap:
        raise middleware.ApiRedirect(obj.fid)
    obj_url = utils.join_url(
        settings.FUNKWHALE_URL,
        utils.spa_reverse("actor_detail",
                          kwargs={"username": obj.preferred_username}),
    )
    metas = [
        {
            "tag": "meta",
            "property": "og:url",
            "content": obj_url
        },
        {
            "tag": "meta",
            "property": "og:title",
            "content": obj.display_name
        },
        {
            "tag": "meta",
            "property": "og:type",
            "content": "profile"
        },
    ]

    if obj.attachment_icon:
        metas.append({
            "tag":
            "meta",
            "property":
            "og:image",
            "content":
            obj.attachment_icon.download_url_medium_square_crop,
        })

    if preferences.get("federation__enabled"):
        metas.append({
            "tag": "link",
            "rel": "alternate",
            "type": "application/activity+json",
            "href": obj.fid,
        })

    return metas
示例#13
0
 def dispatch(self, request, *args, **kwargs):
     if not preferences.get("subsonic__enabled"):
         r = response.Response({}, status=405)
         r.accepted_renderer = renderers.JSONRenderer()
         r.accepted_media_type = "application/json"
         r.renderer_context = {}
         return r
     return super().dispatch(request, *args, **kwargs)
示例#14
0
 def get_permissions(self, defaults=None):
     defaults = defaults or preferences.get("users__default_permissions")
     perms = {}
     for p in PERMISSIONS:
         v = (self.is_superuser or getattr(self, "permission_{}".format(p))
              or p in defaults)
         perms[p] = v
     return perms
示例#15
0
def clean_transcoding_cache():
    delay = preferences.get("music__transcoding_cache_duration")
    if delay < 1:
        return  # cache clearing disabled
    limit = timezone.now() - datetime.timedelta(minutes=delay)
    candidates = (models.UploadVersion.objects.filter(
        (Q(accessed_date__lt=limit) | Q(accessed_date=None))).only(
            "audio_file", "id").order_by("id"))
    return candidates.delete()
示例#16
0
 def inner(*args, **kwargs):
     if not preferences.get("audio__channels_enabled"):
         payload = {
             "error": {
                 "code": 0,
                 "message": "Channels / podcasts are disabled on this pod",
             }
         }
         return response.Response(payload, status=405)
     return f(*args, **kwargs)
示例#17
0
    def has_permission(self, request, view):

        if request.method.lower() in ["options", "head"]:
            return True

        scope_config = getattr(view, "required_scope", "noopscope")
        anonymous_policy = getattr(view, "anonymous_policy", False)
        if anonymous_policy not in [True, False, "setting"]:
            raise ImproperlyConfigured(
                "{} is not a valid value for anonymous_policy".format(anonymous_policy)
            )
        if isinstance(scope_config, str):
            scope_config = {
                "read": "read:{}".format(scope_config),
                "write": "write:{}".format(scope_config),
            }
            action = METHOD_SCOPE_MAPPING[request.method.lower()]
            required_scope = scope_config[action]
        else:
            # we have a dict with explicit viewset actions / scopes
            required_scope = scope_config[view.action]

        token = request.auth

        if isinstance(token, models.AccessToken):
            return self.has_permission_token(token, required_scope)
        elif getattr(request, "scopes", None):
            return should_allow(
                required_scope=required_scope, request_scopes=set(request.scopes)
            )
        elif request.user.is_authenticated:
            user_scopes = scopes.get_from_permissions(**request.user.get_permissions())
            return should_allow(
                required_scope=required_scope, request_scopes=user_scopes
            )
        elif hasattr(request, "actor") and request.actor:
            # we use default anonymous scopes
            user_scopes = scopes.FEDERATION_REQUEST_SCOPES
            return should_allow(
                required_scope=required_scope, request_scopes=user_scopes
            )
        else:
            if anonymous_policy is False:
                return False
            if anonymous_policy == "setting" and preferences.get(
                "common__api_authentication_required"
            ):
                return False

            user_scopes = (
                getattr(view, "anonymous_scopes", set()) | scopes.ANONYMOUS_SCOPES
            )
            return should_allow(
                required_scope=required_scope, request_scopes=user_scopes
            )
示例#18
0
 def get_queryset(self):
     qs = super().get_queryset()
     qs = qs.exclude(
         domain__instance_policy__is_active=True,
         domain__instance_policy__block_all=True,
     )
     if preferences.get("moderation__allow_list_enabled"):
         query = Q(domain_id=settings.FUNKWHALE_HOSTNAME) | Q(
             domain__allowed=True)
         qs = qs.filter(query)
     return qs
示例#19
0
 def nodeinfo(self, request, *args, **kwargs):
     if not preferences.get("instance__nodeinfo_enabled"):
         return HttpResponse(status=404)
     data = {
         "links": [
             {
                 "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
                 "href": utils.full_url(reverse("api:v1:instance:nodeinfo-2.0")),
             }
         ]
     }
     return response.Response(data)
示例#20
0
 def validate_playlist(self, value):
     if self.context.get("request"):
         # validate proper ownership on the playlist
         if self.context["request"].user != value.user:
             raise serializers.ValidationError(
                 "You do not have the permission to edit this playlist")
     existing = value.playlist_tracks.count()
     max_tracks = preferences.get("playlists__max_tracks")
     if existing >= max_tracks:
         raise serializers.ValidationError(
             "Playlist has reached the maximum of {} tracks".format(
                 max_tracks))
     return value
示例#21
0
 def validate(self, validated_data):
     existing_channels = self.context["actor"].owned_channels.count()
     if existing_channels >= preferences.get("audio__max_channels"):
         raise serializers.ValidationError(
             "You have reached the maximum amount of allowed channels")
     validated_data = super().validate(validated_data)
     metadata = validated_data.pop("metadata", {})
     if validated_data["content_category"] == "podcast":
         metadata_serializer = ChannelMetadataSerializer(data=metadata)
         metadata_serializer.is_valid(raise_exception=True)
         metadata = metadata_serializer.validated_data
     validated_data["metadata"] = metadata
     return validated_data
示例#22
0
 def subsonic_token(self, request, *args, **kwargs):
     if not self.request.user.username == kwargs.get("username"):
         return Response(status=403)
     if not preferences.get("subsonic__enabled"):
         return Response(status=405)
     if request.method.lower() == "get":
         return Response(
             {"subsonic_api_token": self.request.user.subsonic_api_token})
     if request.method.lower() == "delete":
         self.request.user.subsonic_api_token = None
         self.request.user.save(update_fields=["subsonic_api_token"])
         return Response(status=204)
     self.request.user.update_subsonic_api_token()
     self.request.user.save(update_fields=["subsonic_api_token"])
     data = {"subsonic_api_token": self.request.user.subsonic_api_token}
     return Response(data)
示例#23
0
def get_actor(fid, skip_cache=False):
    if not skip_cache:
        try:
            actor = models.Actor.objects.select_related().get(fid=fid)
        except models.Actor.DoesNotExist:
            actor = None
        fetch_delta = datetime.timedelta(
            minutes=preferences.get("federation__actor_fetch_delay"))
        if actor and actor.last_fetch_date > timezone.now() - fetch_delta:
            # cache is hot, we can return as is
            return actor
    data = get_actor_data(fid)
    serializer = serializers.ActorSerializer(data=data)
    serializer.is_valid(raise_exception=True)

    return serializer.save(last_fetch_date=timezone.now())
示例#24
0
def dispatch_outbox(activity):
    """
    Deliver a local activity to its recipients, both locally and remotely
    """
    inbox_items = activity.inbox_items.filter(is_read=False).select_related()

    if inbox_items.exists():
        dispatch_inbox.delay(activity_id=activity.pk)

    if not preferences.get("federation__enabled"):
        # federation is disabled, we only deliver to local recipients
        return

    deliveries = activity.deliveries.filter(is_delivered=False)

    for id in deliveries.values_list("pk", flat=True):
        deliver_to_remote.delay(delivery_id=id)
示例#25
0
    def webfinger(self, request, *args, **kwargs):
        if not preferences.get("federation__enabled"):
            return HttpResponse(status=405)
        try:
            resource_type, resource = webfinger.clean_resource(request.GET["resource"])
            cleaner = getattr(webfinger, "clean_{}".format(resource_type))
            result = cleaner(resource)
            handler = getattr(self, "handler_{}".format(resource_type))
            data = handler(result)
        except forms.ValidationError as e:
            return response.Response({"errors": {"resource": e.message}}, status=400)
        except KeyError:
            return response.Response(
                {"errors": {"resource": "This field is required"}}, status=400
            )

        return response.Response(data)
示例#26
0
    def validate(self, validated_data):
        validated_data = super().validate(validated_data)
        submitter = self.context.get("submitter")
        if submitter:
            # we have an authenticated actor so no need to check further
            return validated_data

        unauthenticated_report_types = preferences.get(
            "moderation__unauthenticated_report_types")
        if validated_data["type"] not in unauthenticated_report_types:
            raise serializers.ValidationError(
                "You need an account to submit this report")

        if not validated_data.get("submitter_email"):
            raise serializers.ValidationError(
                "You need to provide an email address to submit this report")

        return validated_data
def main(command, **kwargs):
    open_api = not preferences.get("common__api_authentication_required")
    libraries_by_user = create_libraries(open_api, command.stdout)
    update_uploads(libraries_by_user, command.stdout)
    update_orphan_uploads(open_api, command.stdout)

    set_fid_params = [
        (
            models.Upload.objects.exclude(library__actor__user=None),
            "/federation/music/uploads/",
        ),
        (models.Artist.objects.all(), "/federation/music/artists/"),
        (models.Album.objects.all(), "/federation/music/albums/"),
        (models.Track.objects.all(), "/federation/music/tracks/"),
    ]
    for qs, path in set_fid_params:
        set_fid(qs, path, command.stdout)

    update_shared_inbox_url(command.stdout)

    for part in ["followers", "following"]:
        generate_actor_urls(part, command.stdout)
示例#28
0
    def insert_many(self, tracks, allow_duplicates=True):
        existing = self.playlist_tracks.select_for_update()
        now = timezone.now()
        total = existing.filter(index__isnull=False).count()
        max_tracks = preferences.get("playlists__max_tracks")
        if existing.count() + len(tracks) > max_tracks:
            raise exceptions.ValidationError(
                "Playlist would reach the maximum of {} tracks".format(
                    max_tracks))

        if not allow_duplicates:
            self._check_duplicate_add(existing, tracks)

        self.save(update_fields=["modification_date"])
        start = total
        plts = [
            PlaylistTrack(creation_date=now,
                          playlist=self,
                          track=track,
                          index=start + i) for i, track in enumerate(tracks)
        ]
        return PlaylistTrack.objects.bulk_create(plts)
示例#29
0
    def retrieve(self, request, *args, **kwargs):
        lb = self.get_object()

        conf = {
            "id": lb.get_federation_id(),
            "actor": lb.actor,
            "name": lb.name,
            "summary": lb.description,
            "items": lb.uploads.for_federation().order_by("-creation_date"),
            "item_serializer": serializers.UploadSerializer,
        }
        page = request.GET.get("page")
        if page is None:
            serializer = serializers.LibrarySerializer(lb)
            data = serializer.data
        else:
            # if actor is requesting a specific page, we ensure library is public
            # or readable by the actor
            if not has_library_access(request, lb):
                raise exceptions.AuthenticationFailed(
                    "You do not have access to this library"
                )
            try:
                page_number = int(page)
            except Exception:
                return response.Response({"page": ["Invalid page number"]}, status=400)
            conf["page_size"] = preferences.get("federation__collection_page_size")
            p = paginator.Paginator(conf["items"], conf["page_size"])
            try:
                page = p.page(page_number)
                conf["page"] = page
                serializer = serializers.CollectionPageSerializer(conf)
                data = serializer.data
            except paginator.EmptyPage:
                return response.Response(status=404)

        return response.Response(data)
示例#30
0
 def get(self, request, *args, **kwargs):
     if not preferences.get("instance__nodeinfo_enabled"):
         return Response(status=404)
     data = nodeinfo.get()
     return Response(data, status=200, content_type=NODEINFO_2_CONTENT_TYPE)