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)
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
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)
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)
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)
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"])
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
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
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, )
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)))
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
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
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)
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
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()
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)
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 )
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
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)
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
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
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)
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())
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)
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)
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)
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)
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)
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)