Fix #1038: Federated reports

This commit is contained in:
Eliot Berriot 2020-03-11 11:39:55 +01:00
commit d9afed5067
34 changed files with 985 additions and 76 deletions

View file

@ -456,6 +456,7 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
shared_inbox_url=remote_actor1.shared_inbox_url
)
remote_actor3 = factories["federation.Actor"](shared_inbox_url=None)
remote_actor4 = factories["federation.Actor"]()
library = factories["music.Library"]()
library_follower_local = factories["federation.LibraryFollow"](
@ -491,6 +492,7 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
activity.PUBLIC_ADDRESS,
{"type": "followers", "target": library},
{"type": "followers", "target": followed_actor},
{"type": "actor_inbox", "actor": remote_actor4},
]
inbox_items, deliveries, urls = activity.prepare_deliveries_and_inbox_items(
@ -511,6 +513,7 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
[
models.Delivery(inbox_url=remote_actor1.shared_inbox_url),
models.Delivery(inbox_url=remote_actor3.inbox_url),
models.Delivery(inbox_url=remote_actor4.inbox_url),
models.Delivery(inbox_url=library_follower_remote.inbox_url),
models.Delivery(inbox_url=actor_follower_remote.inbox_url),
],
@ -527,6 +530,7 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
activity.PUBLIC_ADDRESS,
library.followers_url,
followed_actor.followers_url,
remote_actor4.fid,
]
assert urls == expected_urls

View file

@ -67,6 +67,95 @@ def test_expand_no_external_request():
assert doc == expected
def test_expand_no_external_request_pleroma():
payload = {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://pleroma.example/schemas/litepub-0.1.jsonld",
{"@language": "und"},
],
"endpoints": {
"oauthAuthorizationEndpoint": "https://pleroma.example/oauth/authorize",
"oauthRegistrationEndpoint": "https://pleroma.example/api/v1/apps",
"oauthTokenEndpoint": "https://pleroma.example/oauth/token",
"sharedInbox": "https://pleroma.example/inbox",
"uploadMedia": "https://pleroma.example/api/ap/upload_media",
},
"followers": "https://pleroma.example/internal/fetch/followers",
"following": "https://pleroma.example/internal/fetch/following",
"id": "https://pleroma.example/internal/fetch",
"inbox": "https://pleroma.example/internal/fetch/inbox",
"invisible": True,
"manuallyApprovesFollowers": False,
"name": "Pleroma",
"preferredUsername": "internal.fetch",
"publicKey": {
"id": "https://pleroma.example/internal/fetch#main-key",
"owner": "https://pleroma.example/internal/fetch",
"publicKeyPem": "PEM",
},
"summary": "An internal service actor for this Pleroma instance. No user-serviceable parts inside.",
"type": "Application",
"url": "https://pleroma.example/internal/fetch",
}
expected = {
contexts.AS.endpoints: [
{
contexts.AS.sharedInbox: [{"@id": "https://pleroma.example/inbox"}],
contexts.AS.oauthAuthorizationEndpoint: [
{"@id": "https://pleroma.example/oauth/authorize"}
],
contexts.LITEPUB.oauthRegistrationEndpoint: [
{"@id": "https://pleroma.example/api/v1/apps"}
],
contexts.AS.oauthTokenEndpoint: [
{"@id": "https://pleroma.example/oauth/token"}
],
contexts.AS.uploadMedia: [
{"@id": "https://pleroma.example/api/ap/upload_media"}
],
},
],
contexts.AS.followers: [
{"@id": "https://pleroma.example/internal/fetch/followers"}
],
contexts.AS.following: [
{"@id": "https://pleroma.example/internal/fetch/following"}
],
"@id": "https://pleroma.example/internal/fetch",
"http://www.w3.org/ns/ldp#inbox": [
{"@id": "https://pleroma.example/internal/fetch/inbox"}
],
contexts.LITEPUB.invisible: [{"@value": True}],
contexts.AS.manuallyApprovesFollowers: [{"@value": False}],
contexts.AS.name: [{"@language": "und", "@value": "Pleroma"}],
contexts.AS.summary: [
{
"@language": "und",
"@value": "An internal service actor for this Pleroma instance. No user-serviceable parts inside.",
}
],
contexts.AS.url: [{"@id": "https://pleroma.example/internal/fetch"}],
contexts.AS.preferredUsername: [
{"@language": "und", "@value": "internal.fetch"}
],
contexts.SEC.publicKey: [
{
"@id": "https://pleroma.example/internal/fetch#main-key",
contexts.SEC.owner: [{"@id": "https://pleroma.example/internal/fetch"}],
contexts.SEC.publicKeyPem: [{"@language": "und", "@value": "PEM"}],
}
],
"@type": [contexts.AS.Application],
}
doc = jsonld.expand(payload)
assert doc[contexts.AS.endpoints] == expected[contexts.AS.endpoints]
assert doc == expected
def test_expand_remote_doc(r_mock):
url = "https://noop/federation/actors/demo"
payload = {

View file

@ -8,6 +8,7 @@ from funkwhale_api.federation import (
routes,
serializers,
)
from funkwhale_api.moderation import serializers as moderation_serializers
@pytest.mark.parametrize(
@ -30,6 +31,7 @@ from funkwhale_api.federation import (
({"type": "Update", "object": {"type": "Album"}}, routes.inbox_update_album),
({"type": "Update", "object": {"type": "Track"}}, routes.inbox_update_track),
({"type": "Delete", "object": {"type": "Person"}}, routes.inbox_delete_actor),
({"type": "Flag"}, routes.inbox_flag),
],
)
def test_inbox_routes(route, handler):
@ -44,6 +46,7 @@ def test_inbox_routes(route, handler):
"route,handler",
[
({"type": "Accept"}, routes.outbox_accept),
({"type": "Flag"}, routes.outbox_flag),
({"type": "Follow"}, routes.outbox_follow),
({"type": "Create", "object": {"type": "Audio"}}, routes.outbox_create_audio),
(
@ -718,3 +721,69 @@ def test_inbox_delete_actor_doesnt_delete_local_actor(factories):
)
# actor should still be here!
local_actor.refresh_from_db()
@pytest.mark.parametrize(
"factory_name, factory_kwargs",
[
("federation.Actor", {"local": True}),
("music.Artist", {"local": True}),
("music.Album", {"local": True}),
("music.Track", {"local": True}),
("music.Library", {"local": True}),
],
)
def test_inbox_flag(factory_name, factory_kwargs, factories, mocker):
report_created_send = mocker.patch(
"funkwhale_api.moderation.signals.report_created.send"
)
actor = factories["federation.Actor"]()
target = factories[factory_name](**factory_kwargs)
payload = {
"type": "Flag",
"object": [target.fid],
"content": "Test report",
"id": "https://" + actor.domain_id + "/testid",
"actor": actor.fid,
}
serializer = serializers.ActivitySerializer(payload)
result = routes.inbox_flag(
serializer.data, context={"actor": actor, "raise_exception": True}
)
report = actor.reports.latest("id")
assert result == {"object": target, "related_object": report}
assert report.fid == payload["id"]
assert report.type == "other"
assert report.target == target
assert report.target_owner == moderation_serializers.get_target_owner(target)
assert report.target_state == moderation_serializers.get_target_state(target)
report_created_send.assert_called_once_with(sender=None, report=report)
@pytest.mark.parametrize(
"factory_name, factory_kwargs",
[
("federation.Actor", {"local": True}),
("music.Artist", {"local": True}),
("music.Album", {"local": True}),
("music.Track", {"local": True}),
("music.Library", {"local": True}),
],
)
def test_outbox_flag(factory_name, factory_kwargs, factories, mocker):
target = factories[factory_name](**factory_kwargs)
report = factories["moderation.Report"](
target=target, local=True, target_owner=factories["federation.Actor"]()
)
activity = list(routes.outbox_flag({"report": report}))[0]
serializer = serializers.FlagSerializer(report)
expected = serializer.data
expected["to"] = [{"type": "actor_inbox", "actor": report.target_owner}]
assert activity["payload"] == expected
assert activity["actor"] == actors.get_service_actor()

View file

@ -6,12 +6,14 @@ from django.core.paginator import Paginator
from django.utils import timezone
from funkwhale_api.common import utils as common_utils
from funkwhale_api.federation import actors
from funkwhale_api.federation import contexts
from funkwhale_api.federation import keys
from funkwhale_api.federation import jsonld
from funkwhale_api.federation import models
from funkwhale_api.federation import serializers
from funkwhale_api.federation import utils
from funkwhale_api.moderation import serializers as moderation_serializers
from funkwhale_api.music import licenses
@ -70,6 +72,36 @@ def test_actor_serializer_from_ap(db):
assert actor.attachment_icon.mimetype == payload["icon"]["mediaType"]
def test_actor_serializer_from_ap_no_icon_mediaType(db):
private, public = keys.get_key_pair()
actor_url = "https://test.federation/actor"
payload = {
"@context": jsonld.get_default_context_fw(),
"id": actor_url,
"type": "Person",
"inbox": "https://test.com/inbox",
"following": "https://test.com/following",
"followers": "https://test.com/followers",
"preferredUsername": "test",
"manuallyApprovesFollowers": True,
"url": "http://hello.world/path",
"publicKey": {
"publicKeyPem": public.decode("utf-8"),
"owner": actor_url,
"id": actor_url + "#main-key",
},
"endpoints": {"sharedInbox": "https://noop.url/federation/shared/inbox"},
"icon": {"type": "Image", "url": "https://image.example/image.png"},
}
serializer = serializers.ActorSerializer(data=payload)
assert serializer.is_valid(raise_exception=True)
actor = serializer.save()
assert actor.attachment_icon.url == payload["icon"]["url"]
assert actor.attachment_icon.mimetype is None
def test_actor_serializer_only_mandatory_field_from_ap(db):
payload = {
"@context": jsonld.get_default_context(),
@ -1477,3 +1509,44 @@ def test_channel_create_upload_serializer(factories):
serializer = serializers.ChannelCreateUploadSerializer(upload)
assert serializer.data == expected
def test_report_serializer_from_ap_create(factories, faker, now, mocker):
actor = factories["federation.Actor"]()
obj = factories["music.Artist"](local=True)
payload = {
"@context": jsonld.get_default_context(),
"type": "Flag",
"id": "https://test.report",
"actor": actor.fid,
"content": "hello world",
"object": [obj.fid],
"tag": [{"type": "Hashtag", "name": "#offensive_content"}],
}
serializer = serializers.FlagSerializer(data=payload, context={"actor": actor})
assert serializer.is_valid(raise_exception=True) is True
report = serializer.save()
assert report.fid == payload["id"]
assert report.summary == payload["content"]
assert report.submitter == actor
assert report.target == obj
assert report.target_state == moderation_serializers.get_target_state(obj)
assert report.target_owner == moderation_serializers.get_target_owner(obj)
assert report.type == "offensive_content"
def test_report_serializer_to_ap(factories):
report = factories["moderation.Report"](local=True)
expected = {
"@context": jsonld.get_default_context(),
"type": "Flag",
"id": report.fid,
"actor": actors.get_service_actor().fid,
"content": report.summary,
"object": [report.target.fid],
"tag": [{"type": "Hashtag", "name": "#{}".format(report.type)}],
}
serializer = serializers.FlagSerializer(report)
assert serializer.data == expected

View file

@ -0,0 +1,36 @@
from funkwhale_api.common import utils
def test_channel_detail(spa_html, no_api_auth, client, factories, settings):
icon = factories["common.Attachment"]()
actor = factories["federation.Actor"](local=True, attachment_icon=icon)
url = "/@{}".format(actor.preferred_username)
response = client.get(url)
assert response.status_code == 200
expected_metas = [
{
"tag": "meta",
"property": "og:url",
"content": utils.join_url(settings.FUNKWHALE_URL, url),
},
{"tag": "meta", "property": "og:title", "content": actor.display_name},
{"tag": "meta", "property": "og:type", "content": "profile"},
{
"tag": "meta",
"property": "og:image",
"content": actor.attachment_icon.download_url_medium_square_crop,
},
{
"tag": "link",
"rel": "alternate",
"type": "application/activity+json",
"href": actor.fid,
},
]
metas = utils.parse_meta(response.content.decode())
# we only test our custom metas, not the default ones
assert metas[: len(expected_metas)] == expected_metas

View file

@ -0,0 +1,58 @@
from funkwhale_api.federation import serializers
def test_pleroma_actor_from_ap(factories):
payload = {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://test.federation/schemas/litepub-0.1.jsonld",
{"@language": "und"},
],
"endpoints": {
"oauthAuthorizationEndpoint": "https://test.federation/oauth/authorize",
"oauthRegistrationEndpoint": "https://test.federation/api/v1/apps",
"oauthTokenEndpoint": "https://test.federation/oauth/token",
"sharedInbox": "https://test.federation/inbox",
"uploadMedia": "https://test.federation/api/ap/upload_media",
},
"followers": "https://test.federation/internal/fetch/followers",
"following": "https://test.federation/internal/fetch/following",
"id": "https://test.federation/internal/fetch",
"inbox": "https://test.federation/internal/fetch/inbox",
"invisible": True,
"manuallyApprovesFollowers": False,
"name": "Pleroma",
"preferredUsername": "internal.fetch",
"publicKey": {
"id": "https://test.federation/internal/fetch#main-key",
"owner": "https://test.federation/internal/fetch",
"publicKeyPem": "PEM",
},
"summary": "An internal service actor for this Pleroma instance. No user-serviceable parts inside.",
"type": "Application",
"url": "https://test.federation/internal/fetch",
}
serializer = serializers.ActorSerializer(data=payload)
assert serializer.is_valid(raise_exception=True)
actor = serializer.save()
assert actor.fid == payload["id"]
assert actor.url == payload["url"]
assert actor.inbox_url == payload["inbox"]
assert actor.shared_inbox_url == payload["endpoints"]["sharedInbox"]
assert actor.outbox_url is None
assert actor.following_url == payload["following"]
assert actor.followers_url == payload["followers"]
assert actor.followers_url == payload["followers"]
assert actor.type == payload["type"]
assert actor.preferred_username == payload["preferredUsername"]
assert actor.name == payload["name"]
assert actor.summary_obj.text == payload["summary"]
assert actor.summary_obj.content_type == "text/html"
assert actor.fid == payload["url"]
assert actor.manually_approves_followers is payload["manuallyApprovesFollowers"]
assert actor.private_key is None
assert actor.public_key == payload["publicKey"]["publicKeyPem"]
assert actor.domain_id == "test.federation"

View file

@ -1,6 +1,8 @@
from rest_framework import serializers
import pytest
from django.core.exceptions import ObjectDoesNotExist
from funkwhale_api.federation import exceptions, utils
@ -172,3 +174,36 @@ def test_local_qs(factory_name, fids, kwargs, expected_indexes, factories, setti
expected_objs = [obj for i, obj in enumerate(objs) if i in expected_indexes]
assert list(result) == expected_objs
def test_get_obj_by_fid_not_found():
with pytest.raises(ObjectDoesNotExist):
utils.get_object_by_fid("http://test")
def test_get_obj_by_fid_local_not_found(factories):
obj = factories["federation.Actor"](local=False)
with pytest.raises(ObjectDoesNotExist):
utils.get_object_by_fid(obj.fid, local=True)
def test_get_obj_by_fid_local(factories):
obj = factories["federation.Actor"](local=True)
assert utils.get_object_by_fid(obj.fid, local=True) == obj
@pytest.mark.parametrize(
"factory_name",
[
"federation.Actor",
"music.Artist",
"music.Album",
"music.Track",
"music.Upload",
"music.Library",
],
)
def test_get_obj_by_fid(factory_name, factories):
obj = factories[factory_name]()
factories[factory_name]()
assert utils.get_object_by_fid(obj.fid) == obj

View file

@ -354,20 +354,24 @@ def test_music_upload_detail_private_approved_follow(
@pytest.mark.parametrize(
"accept_header,expected",
"accept_header,default,expected",
[
("text/html,application/xhtml+xml", True),
("text/html,application/json", True),
("", False),
(None, False),
("application/json", False),
("application/activity+json", False),
("application/json,text/html", False),
("application/activity+json,text/html", False),
("text/html,application/xhtml+xml", True, True),
("text/html,application/json", True, True),
("", True, False),
(None, True, False),
("application/json", True, False),
("application/activity+json", True, False),
("application/json,text/html", True, False),
("application/activity+json,text/html", True, False),
("unrelated/ct", True, True),
("unrelated/ct", False, False),
],
)
def test_should_redirect_ap_to_html(accept_header, expected):
assert federation_utils.should_redirect_ap_to_html(accept_header) is expected
def test_should_redirect_ap_to_html(accept_header, default, expected):
assert (
federation_utils.should_redirect_ap_to_html(accept_header, default) is expected
)
def test_music_library_retrieve_redirects_to_html_if_header_set(