From 4f83d1bb27a03db177da951dde2b28abf39735ce Mon Sep 17 00:00:00 2001 From: Eliot Berriot <contact@eliotberriot.com> Date: Tue, 28 May 2019 22:40:54 +0200 Subject: [PATCH] Added API endpoint to query profiles --- config/routing.py | 18 ++++++++++++++++ retribute_api/search/consumers.py | 35 +++++++++++++++++++++++++++++++ retribute_api/search/sources.py | 13 ++++++------ retribute_api/search/webfinger.py | 8 +++---- tests/conftest.py | 6 ++++++ tests/search/test_consumers.py | 24 +++++++++++++++++++++ 6 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 config/routing.py create mode 100644 retribute_api/search/consumers.py create mode 100644 tests/search/test_consumers.py diff --git a/config/routing.py b/config/routing.py new file mode 100644 index 0000000..1175710 --- /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 0000000..2cc4e2a --- /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 6275977..e5a4b23 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 db17772..84ed55f 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 6f17a3a..36e3281 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 0000000..dfecbca --- /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() -- GitLab