diff --git a/config/routing.py b/config/routing.py new file mode 100644 index 0000000000000000000000000000000000000000..1175710ebfcf9b462b60b2660c450cdc5348b9e3 --- /dev/null +++ b/config/routing.py @@ -0,0 +1,18 @@ +from django.conf.urls import url + +from channels.routing import ProtocolTypeRouter, URLRouter + +from retribute_api.search import consumers + +application = ProtocolTypeRouter( + { + "http": URLRouter( + [ + url( + r"^api/v1/search/(?P<lookup_type>.+):(?P<lookup>.+)$", + consumers.SearchSingleConsumer, + ) + ] + ) + } +) diff --git a/retribute_api/search/consumers.py b/retribute_api/search/consumers.py new file mode 100644 index 0000000000000000000000000000000000000000..2cc4e2af08917306a03a8f165593a8eb3a2506fc --- /dev/null +++ b/retribute_api/search/consumers.py @@ -0,0 +1,35 @@ +import aiohttp.client +import json + +from channels.generic.http import AsyncHttpConsumer + +from . import sources + + +async def json_response(self, status, content): + await self.send_response( + status, + json.dumps(content, indent=2, sort_keys=True).encode("utf-8"), + headers=[{"Content-Type": "application/json"}], + ) + + +class SearchSingleConsumer(AsyncHttpConsumer): + async def handle(self, body): + lookup_type = self.scope["url_route"]["kwargs"]["lookup_type"] + lookup = self.scope["url_route"]["kwargs"]["lookup"] + try: + source = sources.registry._data[lookup_type] + except KeyError: + await json_response(self, 400, {"detail": "Invalid lookup"}) + try: + async with aiohttp.client.ClientSession() as session: + data = await source.get(lookup, session) + except Exception: + raise + try: + profile = sources.result_to_retribute_profile(lookup_type, lookup, data) + except Exception: + raise + + await json_response(self, 200, profile) diff --git a/retribute_api/search/sources.py b/retribute_api/search/sources.py index 62759774532dcd2fb41f36a925a17efe181f95a9..e5a4b2328f5a2dab60a65c3efb48fead3a6519d8 100644 --- a/retribute_api/search/sources.py +++ b/retribute_api/search/sources.py @@ -34,11 +34,11 @@ class Activitypub(Source): id = "activitypub" async def get(self, lookup, session): - response = await session.get( + async with session.get( lookup, headers={"Accept": "application/activity+json"} - ) - response.raise_for_status() - actor_data = await response.json() + ) as response: + response.raise_for_status() + actor_data = await response.json() serializer = activitypub.ActorSerializer(data=actor_data) serializer.is_valid(raise_exception=True) for tag in serializer.validated_data["tag"]: @@ -64,12 +64,11 @@ class Webfinger(Source): found = None if "activitypub" in links: found = await Activitypub().get(links["activitypub"], session) - return found def result_to_retribute_profile(lookup_type, lookup, data): - path = settings.BASE_URL + "/compat/" + path = settings.BASE_URL + "/compat" now = timezone.now() valid_means = [ (link, means.extract_from_url(link["url"])) for link in data["links"] @@ -89,7 +88,7 @@ def result_to_retribute_profile(lookup_type, lookup, data): final = { "version": "0.1", - "id": "https://retribute.me.test/compat/{}:{}".format(lookup_type, lookup), + "id": "{}/{}:{}".format(path, lookup_type, lookup), "title": "Compat profile for {}:{}".format(lookup_type, lookup), "updated": now.isoformat(), "identities": [], diff --git a/retribute_api/search/webfinger.py b/retribute_api/search/webfinger.py index db1777217f9987eca71246741107c4010fd82a74..84ed55f38e5bfa203875e23e1a0c29ca1165fa90 100644 --- a/retribute_api/search/webfinger.py +++ b/retribute_api/search/webfinger.py @@ -3,12 +3,12 @@ from rest_framework import serializers async def lookup(name, session): username, domain = name.split("@") - response = await session.get( + async with session.get( "https://{}/.well-known/webfinger".format(domain), params={"resource": "acct:{}".format(name)}, - ) - response.raise_for_status() - return await response.json() + ) as response: + response.raise_for_status() + return await response.json() class AccountLinkSerializer(serializers.Serializer): diff --git a/tests/conftest.py b/tests/conftest.py index 6f17a3a691834170b1723adbb505d437102d39ed..36e328199c73e08f579f4740ea4507cb26cff714 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ import asynctest from django.utils import timezone +from config import routing pytest_plugins = "aiohttp.pytest_plugin" @@ -31,3 +32,8 @@ def now(mocker): now = timezone.now() mocker.patch.object(timezone, "now", return_value=now) return now + + +@pytest.fixture +def application(): + return routing.application diff --git a/tests/search/test_consumers.py b/tests/search/test_consumers.py new file mode 100644 index 0000000000000000000000000000000000000000..dfecbca2ce1627da2769021ec2715460d670b29c --- /dev/null +++ b/tests/search/test_consumers.py @@ -0,0 +1,24 @@ +import json +from channels.testing import HttpCommunicator + +from retribute_api.search import consumers +from retribute_api.search import sources + + +async def test_search_consumer_success(loop, application, mocker, coroutine_mock): + get = mocker.patch.object(sources.Webfinger, "get", coroutine_mock()) + expected = {"dummy": "json"} + get_profile = mocker.patch.object( + sources, "result_to_retribute_profile", return_value=expected + ) + communicator = HttpCommunicator( + application, "GET", "/api/v1/search/webfinger:test@user.domain" + ) + response = await communicator.get_response() + assert get.call_args[0][0] == "test@user.domain" + get_profile.assert_called_once_with( + "webfinger", "test@user.domain", get.return_value + ) + assert response["status"] == 200 + assert response["headers"] == [{"Content-Type": "application/json"}] + assert response["body"] == json.dumps(expected, indent=2, sort_keys=True).encode()