def test_report_created_signal_sends_email_to_mods(factories, mailoutbox, settings): mod1 = factories["users.User"](permission_moderation=True) mod2 = factories["users.User"](permission_moderation=True) # inactive, so no email factories["users.User"](permission_moderation=True, is_active=False) # no moderation permission, so no email factories["users.User"]() report = factories["moderation.Report"]() tasks.send_new_report_email_to_moderators(report_id=report.pk) detail_url = federation_utils.full_url( "/manage/moderation/reports/{}".format(report.uuid)) unresolved_reports_url = federation_utils.full_url( "/manage/moderation/reports?q=resolved:no") assert len(mailoutbox) == 2 for i, mod in enumerate([mod1, mod2]): m = mailoutbox[i] assert m.subject == "[{} moderation - {}] New report from {}".format( settings.FUNKWHALE_HOSTNAME, report.get_type_display(), report.submitter.full_username, ) assert report.summary in m.body assert report.target._meta.verbose_name.title() in m.body assert str(report.target) in m.body assert report.target.get_absolute_url() in m.body assert report.target.get_moderation_url() in m.body assert detail_url in m.body assert unresolved_reports_url in m.body assert list(m.to) == [mod.email]
def test_attachment_serializer_remote_file(factories, to_api_date): attachment = factories["common.Attachment"](file=None) proxy_url = reverse("api:v1:attachments-proxy", kwargs={"uuid": attachment.uuid}) expected = { "uuid": str(attachment.uuid), "size": attachment.size, "mimetype": attachment.mimetype, "creation_date": to_api_date(attachment.creation_date), # everything is the same, except for the urls field because: # - the file isn't available on the local pod # - we need to return different URLs so that the client can trigger # a fetch and get redirected to the desired version # "urls": { "source": attachment.url, "original": federation_utils.full_url(proxy_url + "?next=original"), "medium_square_crop": federation_utils.full_url( proxy_url + "?next=medium_square_crop" ), "large_square_crop": federation_utils.full_url( proxy_url + "?next=large_square_crop" ), }, } serializer = serializers.AttachmentSerializer(attachment) assert serializer.data == expected
def get_actor_data(username, **kwargs): slugified_username = federation_utils.slugify_username(username) domain = kwargs.get("domain") if not domain: domain = federation_models.Domain.objects.get_or_create( name=settings.FEDERATION_HOSTNAME)[0] return { "preferred_username": slugified_username, "domain": domain, "type": "Person", "name": kwargs.get("name", username), "summary": kwargs.get("summary"), "manually_approves_followers": False, "fid": federation_utils.full_url( reverse( "federation:actors-detail", kwargs={"preferred_username": slugified_username}, )), "shared_inbox_url": federation_models.get_shared_inbox_url(), "inbox_url": federation_utils.full_url( reverse( "federation:actors-inbox", kwargs={"preferred_username": slugified_username}, )), "outbox_url": federation_utils.full_url( reverse( "federation:actors-outbox", kwargs={"preferred_username": slugified_username}, )), "followers_url": federation_utils.full_url( reverse( "federation:actors-followers", kwargs={"preferred_username": slugified_username}, )), "following_url": federation_utils.full_url( reverse( "federation:actors-following", kwargs={"preferred_username": slugified_username}, )), }
def test_can_create_track_from_api(artists, albums, tracks, mocker, db): mocker.patch( "funkwhale_api.musicbrainz.api.artists.get", return_value=artists["get"]["adhesive_wombat"], ) mocker.patch( "funkwhale_api.musicbrainz.api.releases.get", return_value=albums["get"]["marsupial"], ) mocker.patch( "funkwhale_api.musicbrainz.api.recordings.search", return_value=tracks["search"]["8bitadventures"], ) track = models.Track.create_from_api(query="8-bit adventure") data = models.Track.api.search(query="8-bit adventure")["recording-list"][0] assert int(data["ext:score"]) == 100 assert data["id"] == "9968a9d6-8d92-4051-8f76-674e157b6eed" assert track.mbid == data["id"] assert track.artist.pk is not None assert str(track.artist.mbid) == "62c3befb-6366-4585-b256-809472333801" assert track.artist.name == "Adhesive Wombat" assert str(track.album.mbid) == "a50d2a81-2a50-484d-9cb4-b9f6833f583e" assert track.album.title == "Marsupial Madness" assert track.fid == federation_utils.full_url( "/federation/music/tracks/{}".format(track.uuid) )
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("/channels/{}".format(suffix))
def download_url_medium_square_crop(self): if self.file: return utils.media_url(self.file.crop["200x200"].url) proxy_url = reverse("api:v1:attachments-proxy", kwargs={"uuid": self.uuid}) return federation_utils.full_url(proxy_url + "?next=medium_square_crop")
def get_federation_id(self): if self.fid: return self.fid return federation_utils.full_url( reverse("federation:music:uploads-detail", kwargs={"uuid": self.uuid}))
def test_activity_pub_album_serializer_to_ap(factories): album = factories["music.Album"]() expected = { "@context": serializers.AP_CONTEXT, "type": "Album", "id": album.fid, "name": album.title, "cover": { "type": "Link", "mediaType": "image/jpeg", "href": utils.full_url(album.cover.url), }, "musicbrainzId": album.mbid, "published": album.creation_date.isoformat(), "released": album.release_date.isoformat(), "artists": [ serializers.ArtistSerializer(album.artist, context={ "include_ap_context": False }).data ], } serializer = serializers.AlbumSerializer(album) assert serializer.data == expected
def channels(self, request, *args, **kwargs): actors = (models.Actor.objects.local().exclude( channel=None).order_by("channel__creation_date").prefetch_related( "channel__attributed_to", "channel__artist", "channel__artist__description", "channel__artist__attachment_cover", )) conf = { "id": federation_utils.full_url( reverse("federation:index:index-channels")), "items": actors, "item_serializer": serializers.ActorSerializer, "page_size": 100, "actor": None, } return get_collection_response( conf=conf, querystring=request.GET, collection_serializer=serializers.IndexSerializer(conf), ) return response.Response({}, status=200)
def test_oembed_channel(factories, no_api_auth, api_client, settings): settings.FUNKWHALE_URL = "http://test" settings.FUNKWHALE_EMBED_URL = "http://embed" channel = factories["audio.Channel"](artist__with_cover=True) artist = channel.artist url = reverse("api:v1:oembed") obj_url = "https://test.com/channels/{}".format(channel.uuid) iframe_src = "http://embed?type=channel&id={}".format(channel.uuid) expected = { "version": "1.0", "type": "rich", "provider_name": settings.APP_NAME, "provider_url": settings.FUNKWHALE_URL, "height": 400, "width": 600, "title": artist.name, "description": artist.name, "thumbnail_url": federation_utils.full_url( artist.attachment_cover.file.crop["200x200"].url), "thumbnail_height": 200, "thumbnail_width": 200, "html": '<iframe width="600" height="400" scrolling="no" frameborder="no" src="{}"></iframe>' .format(iframe_src), "author_name": artist.name, "author_url": federation_utils.full_url( utils.spa_reverse("channel_detail", kwargs={"uuid": channel.uuid})), } response = api_client.get(url, {"url": obj_url, "format": "json"}) assert response.data == expected
def test_channel_get_rss_url_local(factories): channel = factories["audio.Channel"](artist__local=True) expected = federation_utils.full_url( reverse( "api:v1:channels-rss", kwargs={"composite": channel.actor.preferred_username}, )) assert channel.get_rss_url() == expected
def media_url(path): if settings.MEDIA_URL.startswith( "http://") or settings.MEDIA_URL.startswith("https://"): return join_url(settings.MEDIA_URL, path) from funkwhale_api.federation import utils as federation_utils return federation_utils.full_url(path)
def get_federation_id(self): if self.fid: return self.fid return federation_utils.full_url( reverse( "federation:music:{}-detail".format(self.federation_namespace), kwargs={"uuid": self.uuid}, ))
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 serve_spa(request): html = get_spa_html(settings.FUNKWHALE_SPA_HTML_ROOT) head, tail = html.split("</head>", 1) if settings.FUNKWHALE_SPA_REWRITE_MANIFEST: new_url = (settings.FUNKWHALE_SPA_REWRITE_MANIFEST_URL or federation_utils.full_url( urls.reverse("api:v1:instance:spa-manifest"))) title = preferences.get("instance__name") if title: head = replace_title(head, title) head = replace_manifest_url(head, new_url) if not preferences.get("common__api_authentication_required"): try: request_tags = get_request_head_tags(request) or [] except urls.exceptions.Resolver404: # we don't have any custom tags for this route request_tags = [] else: # API is not open, we don't expose any custom data request_tags = [] default_tags = get_default_head_tags(request.path) unique_attributes = ["name", "property"] final_tags = request_tags skip = [] for t in final_tags: for attr in unique_attributes: if attr in t: skip.append(t[attr]) for t in default_tags: existing = False for attr in unique_attributes: if t.get(attr) in skip: existing = True break if not existing: final_tags.append(t) # let's inject our meta tags in the HTML head += "\n" + "\n".join(render_tags(final_tags)) + "\n</head>" css = get_custom_css() or "" if css: # We add the style add the end of the body to ensure it has the highest # priority (since it will come after other stylesheets) body, tail = tail.split("</body>", 1) css = "<style>{}</style>".format(css) tail = body + "\n" + css + "\n</body>" + tail # set a csrf token so that visitor can login / query API if needed token = csrf.get_token(request) response = http.HttpResponse(head + tail) response.set_cookie("csrftoken", token, max_age=None) return response
def notify_mods_signup_request_pending(obj): moderators = get_moderators() submitter_repr = obj.submitter.preferred_username subject = "[{} moderation] New sign-up request from {}".format( settings.FUNKWHALE_HOSTNAME, submitter_repr ) detail_url = federation_utils.full_url( "/manage/moderation/requests/{}".format(obj.uuid) ) unresolved_requests_url = federation_utils.full_url( "/manage/moderation/requests?q=status:pending" ) unresolved_requests = models.UserRequest.objects.filter(status="pending").count() body = [ "{} wants to register on your pod. You need to review their request before they can use the service.".format( submitter_repr ), "", "- To handle this request, please visit {}".format(detail_url), "- To view all unresolved requests (currently {}), please visit {}".format( unresolved_requests, unresolved_requests_url ), "", "—", "", "You are receiving this email because you are a moderator for {}.".format( settings.FUNKWHALE_HOSTNAME ), ] for moderator in moderators: if not moderator.email: logger.warning("Moderator %s has no email configured", moderator.username) continue mail.send_mail( subject, message="\n".join(body), recipient_list=[moderator.email], from_email=settings.DEFAULT_FROM_EMAIL, )
def test_attachment_serializer_existing_file(factories, to_api_date): attachment = factories["common.Attachment"]() expected = { "uuid": str(attachment.uuid), "size": attachment.size, "mimetype": attachment.mimetype, "creation_date": to_api_date(attachment.creation_date), "urls": { "source": attachment.url, "original": federation_utils.full_url(attachment.file.url), "medium_square_crop": federation_utils.full_url(attachment.file.crop["200x200"].url), }, # XXX: BACKWARD COMPATIBILITY "original": federation_utils.full_url(attachment.file.url), "medium_square_crop": federation_utils.full_url(attachment.file.crop["200x200"].url), "small_square_crop": federation_utils.full_url(attachment.file.crop["200x200"].url), "square_crop": federation_utils.full_url(attachment.file.crop["200x200"].url), } serializer = serializers.AttachmentSerializer(attachment) assert serializer.data == expected
def rss_serialize_item(upload): data = { "title": [{"value": upload.track.title}], "itunes:title": [{"value": upload.track.title}], "guid": [{"cdata_value": str(upload.uuid), "isPermalink": "false"}], "pubDate": [{"value": rfc822_date(upload.creation_date)}], "itunes:duration": [{"value": rss_duration(upload.duration)}], "itunes:explicit": [{"value": "no"}], "itunes:episodeType": [{"value": "full"}], "itunes:season": [{"value": upload.track.disc_number or 1}], "itunes:episode": [{"value": upload.track.position or 1}], "link": [{"value": federation_utils.full_url(upload.track.get_absolute_url())}], "enclosure": [ { # we enforce MP3, since it's the only format supported everywhere "url": federation_utils.full_url(upload.get_listen_url(to="mp3")), "length": upload.size or 0, "type": "audio/mpeg", } ], } if upload.track.description: data["itunes:subtitle"] = [{"value": upload.track.description.truncate(255)}] data["itunes:summary"] = [{"cdata_value": upload.track.description.rendered}] data["description"] = [{"value": upload.track.description.as_plain_text}] if upload.track.attachment_cover: data["itunes:image"] = [ {"href": upload.track.attachment_cover.download_url_original} ] tagged_items = getattr(upload.track, "_prefetched_tagged_items", []) if tagged_items: data["itunes:keywords"] = [ {"value": " ".join([ti.tag.name for ti in tagged_items])} ] 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_actor_data(user): username = federation_utils.slugify_username(user.username) return { "preferred_username": username, "domain": settings.FEDERATION_HOSTNAME, "type": "Person", "name": user.username, "manually_approves_followers": False, "fid": federation_utils.full_url( reverse("federation:actors-detail", kwargs={"preferred_username": username})), "shared_inbox_url": federation_models.get_shared_inbox_url(), "inbox_url": federation_utils.full_url( reverse("federation:actors-inbox", kwargs={"preferred_username": username})), "outbox_url": federation_utils.full_url( reverse("federation:actors-outbox", kwargs={"preferred_username": username})), "followers_url": federation_utils.full_url( reverse("federation:actors-followers", kwargs={"preferred_username": username})), "following_url": federation_utils.full_url( reverse("federation:actors-following", kwargs={"preferred_username": username})), }
def test_signup_request_pending_sends_email_to_mods(factories, mailoutbox, settings): mod1 = factories["users.User"](permission_moderation=True) mod2 = factories["users.User"](permission_moderation=True) signup_request = factories["moderation.UserRequest"](signup=True) tasks.user_request_handle(user_request_id=signup_request.pk, new_status="pending") detail_url = federation_utils.full_url( "/manage/moderation/requests/{}".format(signup_request.uuid)) unresolved_requests_url = federation_utils.full_url( "/manage/moderation/requests?q=status:pending") assert len(mailoutbox) == 2 for i, mod in enumerate([mod1, mod2]): m = mailoutbox[i] assert m.subject == "[{} moderation] New sign-up request from {}".format( settings.FUNKWHALE_HOSTNAME, signup_request.submitter.preferred_username, ) assert detail_url in m.body assert unresolved_requests_url in m.body assert list(m.to) == [mod.email]
def test_can_create_artist_from_api(artists, mocker, db): mocker.patch( "musicbrainzngs.search_artists", return_value=artists["search"]["adhesive_wombat"], ) artist = models.Artist.create_from_api(query="Adhesive wombat") data = models.Artist.api.search(query="Adhesive wombat")["artist-list"][0] assert int(data["ext:score"]), 100 assert data["id"], "62c3befb-6366-4585-b256-809472333801" assert artist.mbid, data["id"] assert artist.name, "Adhesive Wombat" assert artist.fid == federation_utils.full_url( "/federation/music/artists/{}".format(artist.uuid) )
def test_rewrite_manifest_json_url_rewrite_default_url(mocker, settings): settings.FUNKWHALE_SPA_REWRITE_MANIFEST = True settings.FUNKWHALE_SPA_REWRITE_MANIFEST_URL = None spa_html = "<html><head><link href=/manifest.json rel=manifest></head></html>" expected_url = federation_utils.full_url( reverse("api:v1:instance:spa-manifest")) request = mocker.Mock(path="/") mocker.patch.object(middleware, "get_spa_html", return_value=spa_html) mocker.patch.object( middleware, "get_default_head_tags", return_value=[], ) response = middleware.serve_spa(request) assert response.status_code == 200 expected_html = '<html><head><link rel=manifest href="{}">\n\n</head></html>'.format( expected_url) assert response.content == expected_html.encode()
def test_activity_pub_audio_serializer_to_ap(factories): upload = factories["music.Upload"](mimetype="audio/mp3", bitrate=42, duration=43, size=44) expected = { "@context": serializers.AP_CONTEXT, "type": "Audio", "id": upload.fid, "name": upload.track.full_name, "published": upload.creation_date.isoformat(), "updated": upload.modification_date.isoformat(), "duration": upload.duration, "bitrate": upload.bitrate, "size": upload.size, "url": { "href": utils.full_url(upload.listen_url), "type": "Link", "mediaType": "audio/mp3", }, "library": upload.library.fid, "track": serializers.TrackSerializer(upload.track, context={ "include_ap_context": False }).data, } serializer = serializers.UploadSerializer(upload) assert serializer.data == expected
def test_rss_channel_serializer_placeholder_image(factories): description = factories["common.Content"]() channel = factories["audio.Channel"]( artist__set_tags=["pop", "rock"], artist__description=description, artist__attachment_cover=None, ) setattr( channel.artist, "_prefetched_tagged_items", channel.artist.tagged_items.order_by("tag__name"), ) expected = [{ "href": federation_utils.full_url( static("images/podcasts-cover-placeholder.png")) }] assert serializers.rss_serialize_channel( channel)["itunes:image"] == expected
def test_approved_request_sends_email_to_submitter_and_set_active( factories, mailoutbox, settings): user = factories["users.User"](is_active=False) actor = user.create_actor() signup_request = factories["moderation.UserRequest"](signup=True, submitter=actor, status="approved") tasks.user_request_handle(user_request_id=signup_request.pk, new_status="approved") user.refresh_from_db() assert user.is_active is True assert len(mailoutbox) == 1 m = mailoutbox[-1] login_url = federation_utils.full_url("/login") assert m.subject == "Welcome to {}, {}!".format( settings.FUNKWHALE_HOSTNAME, signup_request.submitter.preferred_username, ) assert login_url in m.body assert list(m.to) == [user.email]
def notify_submitter_signup_request_approved(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 = "Welcome to {}, {}!".format(settings.FUNKWHALE_HOSTNAME, submitter_repr) login_url = federation_utils.full_url("/login") body = [ "Hi {} and welcome,".format(submitter_repr), "", "Our moderation team has approved your account request and you can now start " "using the service. Please visit {} to get started.".format(login_url), "", "Before your first login, you may need to verify your email address if you didn't already.", ] mail.send_mail( subject, message="\n".join(body), recipient_list=[submitter_email], from_email=settings.DEFAULT_FROM_EMAIL, )
def test_can_create_album_from_api(artists, albums, mocker, db): mocker.patch( "funkwhale_api.musicbrainz.api.releases.search", return_value=albums["search"]["hypnotize"], ) mocker.patch( "funkwhale_api.musicbrainz.api.artists.get", return_value=artists["get"]["soad"] ) album = models.Album.create_from_api( query="Hypnotize", artist="system of a down", type="album" ) data = models.Album.api.search( query="Hypnotize", artist="system of a down", type="album" )["release-list"][0] assert album.mbid, data["id"] assert album.title, "Hypnotize" assert album.release_date, datetime.date(2005, 1, 1) assert album.artist.name, "System of a Down" assert album.artist.mbid, data["artist-credit"][0]["artist"]["id"] assert album.fid == federation_utils.full_url( "/federation/music/albums/{}".format(album.uuid) )
def libraries(self, request, *args, **kwargs): libraries = (music_models.Library.objects.local().filter( channel=None, privacy_level="everyone").prefetch_related( "actor").order_by("creation_date")) conf = { "id": federation_utils.full_url( reverse("federation:index:index-libraries")), "items": libraries, "item_serializer": serializers.LibrarySerializer, "page_size": 100, "actor": None, } return get_collection_response( conf=conf, querystring=request.GET, collection_serializer=serializers.IndexSerializer(conf), ) return response.Response({}, status=200)
def test_creating_actor_from_user(factories, settings): user = factories["users.User"](username="******") actor = models.create_actor(user) assert actor.preferred_username == "Hello_M_world" # slugified assert actor.domain.pk == settings.FEDERATION_HOSTNAME assert actor.type == "Person" assert actor.name == user.username assert actor.manually_approves_followers is False assert actor.fid == federation_utils.full_url( reverse( "federation:actors-detail", kwargs={"preferred_username": actor.preferred_username}, )) assert actor.shared_inbox_url == federation_utils.full_url( reverse("federation:shared-inbox")) assert actor.inbox_url == federation_utils.full_url( reverse( "federation:actors-inbox", kwargs={"preferred_username": actor.preferred_username}, )) assert actor.outbox_url == federation_utils.full_url( reverse( "federation:actors-outbox", kwargs={"preferred_username": actor.preferred_username}, )) assert actor.followers_url == federation_utils.full_url( reverse( "federation:actors-followers", kwargs={"preferred_username": actor.preferred_username}, )) assert actor.following_url == federation_utils.full_url( reverse( "federation:actors-following", kwargs={"preferred_username": actor.preferred_username}, ))