Merge pull request #123 from jaywink/nodeinfo2

Add NodeInfo2 generator and Django view
merge-requests/130/head
Jason Robinson 2018-04-08 15:15:58 +03:00 zatwierdzone przez GitHub
commit 3bf19d2a8b
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
7 zmienionych plików z 157 dodań i 13 usunięć

Wyświetl plik

@ -14,6 +14,8 @@
* Add fetchers and parsers for NodeInfo, NodeInfo2 and StatisticsJSON server metainfo documents. * Add fetchers and parsers for NodeInfo, NodeInfo2 and StatisticsJSON server metainfo documents.
* Add NodeInfo2 generator and Django view. See documentation for details. ([related issue](https://github.com/jaywink/federation/issues/32))
### Changed ### Changed
* Send outbound Diaspora payloads in new format. Remove possibility to generate legacy MagicEnvelope payloads. ([related issue](https://github.com/jaywink/federation/issues/82)) * Send outbound Diaspora payloads in new format. Remove possibility to generate legacy MagicEnvelope payloads. ([related issue](https://github.com/jaywink/federation/issues/82))

Wyświetl plik

@ -52,6 +52,7 @@ Helper methods
.. autofunction:: federation.hostmeta.generators.generate_host_meta .. autofunction:: federation.hostmeta.generators.generate_host_meta
.. autofunction:: federation.hostmeta.generators.generate_legacy_webfinger .. autofunction:: federation.hostmeta.generators.generate_legacy_webfinger
.. autofunction:: federation.hostmeta.generators.generate_hcard .. autofunction:: federation.hostmeta.generators.generate_hcard
.. autofunction:: federation.hostmeta.generators.generate_nodeinfo2_document
.. autofunction:: federation.hostmeta.generators.get_nodeinfo_well_known_document .. autofunction:: federation.hostmeta.generators.get_nodeinfo_well_known_document
Generator classes Generator classes
@ -98,6 +99,7 @@ Some ready provided views and URL configuration exist for Django.
Note! Django is not part of the normal requirements for this library. It must be installed separately. Note! Django is not part of the normal requirements for this library. It must be installed separately.
.. autofunction:: federation.hostmeta.django.generators.rfc3033_webfinger_view .. autofunction:: federation.hostmeta.django.generators.rfc3033_webfinger_view
.. autofunction:: federation.hostmeta.django.generators.nodeinfo2_view
Configuration Configuration
............. .............
@ -115,6 +117,7 @@ Some settings need to be set in Django settings. An example is below:
FEDERATION = { FEDERATION = {
"base_url": "https://myserver.domain.tld, "base_url": "https://myserver.domain.tld,
"get_profile_function": "myproject.utils.get_profile_by_handle", "get_profile_function": "myproject.utils.get_profile_by_handle",
"nodeinfo2_function": "myproject.utils.get_nodeinfo2_data",
"search_path": "/search/?q=", "search_path": "/search/?q=",
} }
@ -125,6 +128,18 @@ Some settings need to be set in Django settings. An example is below:
* ``profile_path`` - profile path for generating an absolute URL to the profile page of the user. * ``profile_path`` - profile path for generating an absolute URL to the profile page of the user.
* ``atom_path`` - (optional) atom feed path for the profile * ``atom_path`` - (optional) atom feed path for the profile
* ``nodeinfo2_function`` (optional) function that returns data for generating a `NodeInfo2 document <https://github.com/jaywink/nodeinfo2>`_. Once configured the path ``/.well-known/x-nodeinfo2`` will automatically generate a NodeInfo2 document. The function should return a ``dict`` corresponding to the NodeInfo2 schema, with the following minimum items:
::
{server:
baseUrl
name
software
version
}
openRegistrations
* ``search_path`` (optional) site search path which ends in a parameter for search input, for example "/search?q=" * ``search_path`` (optional) site search path which ends in a parameter for search input, for example "/search?q="
Protocols Protocols
@ -154,9 +169,9 @@ Diaspora
.. autofunction:: federation.utils.diaspora.parse_profile_diaspora_id .. autofunction:: federation.utils.diaspora.parse_profile_diaspora_id
.. autofunction:: federation.utils.diaspora.parse_profile_from_hcard .. autofunction:: federation.utils.diaspora.parse_profile_from_hcard
.. autofunction:: federation.utils.diaspora.retrieve_and_parse_content .. autofunction:: federation.utils.diaspora.retrieve_and_parse_content
.. autofunction:: federation.utils.diaspora.retrieve_and_parse_diaspora_webfinger
.. autofunction:: federation.utils.diaspora.retrieve_and_parse_profile .. autofunction:: federation.utils.diaspora.retrieve_and_parse_profile
.. autofunction:: federation.utils.diaspora.retrieve_diaspora_hcard .. autofunction:: federation.utils.diaspora.retrieve_diaspora_hcard
.. autofunction:: federation.utils.diaspora.retrieve_diaspora_webfinger
.. autofunction:: federation.utils.diaspora.retrieve_diaspora_host_meta .. autofunction:: federation.utils.diaspora.retrieve_diaspora_host_meta
Network Network

Wyświetl plik

@ -5,7 +5,7 @@ from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseBadRequest, JsonResponse, HttpResponseNotFound from django.http import HttpResponseBadRequest, JsonResponse, HttpResponseNotFound
from federation.hostmeta.generators import RFC3033Webfinger from federation.hostmeta.generators import RFC3033Webfinger, generate_nodeinfo2_document
logger = logging.getLogger("federation") logger = logging.getLogger("federation")
@ -16,6 +16,7 @@ def get_configuration():
""" """
configuration = { configuration = {
"hcard_path": "/hcard/users/", "hcard_path": "/hcard/users/",
"nodeinfo2_function": None,
"search_path": None, "search_path": None,
} }
configuration.update(settings.FEDERATION) configuration.update(settings.FEDERATION)
@ -27,16 +28,26 @@ def get_configuration():
return configuration return configuration
def get_profile_func(): def get_function_from_config(item):
""" """
Import the function to get profile by handle. Import the function to get profile by handle.
""" """
config = get_configuration() config = get_configuration()
profile_func_path = config.get("get_profile_function") func_path = config.get(item)
module_path, func_name = profile_func_path.rsplit(".", 1) module_path, func_name = func_path.rsplit(".", 1)
module = importlib.import_module(module_path) module = importlib.import_module(module_path)
profile_func = getattr(module, func_name) func = getattr(module, func_name)
return profile_func return func
def nodeinfo2_view(request, *args, **kwargs):
try:
nodeinfo2_func = get_function_from_config("nodeinfo2_function")
except AttributeError:
return HttpResponseBadRequest("Not configured")
nodeinfo2 = nodeinfo2_func()
return JsonResponse(generate_nodeinfo2_document(**nodeinfo2))
def rfc3033_webfinger_view(request, *args, **kwargs): def rfc3033_webfinger_view(request, *args, **kwargs):
@ -50,7 +61,7 @@ def rfc3033_webfinger_view(request, *args, **kwargs):
return HttpResponseBadRequest("Invalid resource") return HttpResponseBadRequest("Invalid resource")
handle = resource.replace("acct:", "") handle = resource.replace("acct:", "")
profile_func = get_profile_func() profile_func = get_function_from_config("get_profile_function")
try: try:
profile = profile_func(handle) profile = profile_func(handle)

Wyświetl plik

@ -1,7 +1,9 @@
from django.conf.urls import url from django.conf.urls import url
from federation.hostmeta.django import rfc3033_webfinger_view from federation.hostmeta.django import rfc3033_webfinger_view
from federation.hostmeta.django.generators import nodeinfo2_view
urlpatterns = [ urlpatterns = [
url(r'^.well-known/webfinger$', rfc3033_webfinger_view, name="rfc3033-webfinger"), url(r'^.well-known/webfinger$', rfc3033_webfinger_view, name="rfc3033-webfinger"),
url(r'^.well-known/x-nodeinfo2$', nodeinfo2_view, name="nodeinfo2"),
] ]

Wyświetl plik

@ -41,6 +41,60 @@ def generate_legacy_webfinger(template=None, *args, **kwargs):
return webfinger.render() return webfinger.render()
def generate_nodeinfo2_document(**kwargs):
"""
Generate a NodeInfo2 document.
Pass in a dictionary as per NodeInfo2 1.0 schema:
https://github.com/jaywink/nodeinfo2/blob/master/schemas/1.0/schema.json
Minimum required schema:
{server:
baseUrl
name
software
version
}
openRegistrations
Protocols default will match what this library supports, ie "diaspora" currently.
:return: dict
:raises: KeyError on missing required items
"""
return {
"version": "1.0",
"server": {
"baseUrl": kwargs['server']['baseUrl'],
"name": kwargs['server']['name'],
"software": kwargs['server']['software'],
"version": kwargs['server']['version'],
},
"organization": {
"name": kwargs.get('organization', {}).get('name', None),
"contact": kwargs.get('organization', {}).get('contact', None),
"account": kwargs.get('organization', {}).get('account', None),
},
"protocols": kwargs.get('protocols', ["diaspora"]),
"relay": kwargs.get('relay', ''),
"services": {
"inbound": kwargs.get('service', {}).get('inbound', []),
"outbound": kwargs.get('service', {}).get('outbound', []),
},
"openRegistrations": kwargs['openRegistrations'],
"usage": {
"users": {
"total": kwargs.get('usage', {}).get('users', {}).get('total'),
"activeHalfyear": kwargs.get('usage', {}).get('users', {}).get('activeHalfyear'),
"activeMonth": kwargs.get('usage', {}).get('users', {}).get('activeMonth'),
"activeWeek": kwargs.get('usage', {}).get('users', {}).get('activeWeek'),
},
"localPosts": kwargs.get('usage', {}).get('localPosts'),
"localComments": kwargs.get('usage', {}).get('localComments'),
}
}
def generate_hcard(template=None, **kwargs): def generate_hcard(template=None, **kwargs):
"""Generate a hCard document. """Generate a hCard document.

Wyświetl plik

@ -4,14 +4,29 @@ from unittest.mock import patch, Mock
from django.test import RequestFactory from django.test import RequestFactory
from federation.hostmeta.django import rfc3033_webfinger_view from federation.hostmeta.django import rfc3033_webfinger_view
from federation.hostmeta.django.generators import get_profile_func from federation.hostmeta.django.generators import get_function_from_config, nodeinfo2_view
from federation.tests.fixtures.hostmeta import NODEINFO2_10_DOC
def test_get_profile_func(): def test_get_function_from_config():
func = get_profile_func() func = get_function_from_config("get_profile_function")
assert callable(func) assert callable(func)
class TestNodeInfo2View:
def test_returns_400_if_not_configured(self):
request = RequestFactory().get('/.well-known/x-nodeinfo2')
response = nodeinfo2_view(request)
assert response.status_code == 400
@patch("federation.hostmeta.django.generators.get_function_from_config")
def test_returns_200(self, mock_get_func):
mock_get_func.return_value = Mock(return_value=json.loads(NODEINFO2_10_DOC))
request = RequestFactory().get('/.well-known/x-nodeinfo2')
response = nodeinfo2_view(request)
assert response.status_code == 200
class TestRFC3033WebfingerView: class TestRFC3033WebfingerView:
def test_no_resource_returns_bad_request(self): def test_no_resource_returns_bad_request(self):
request = RequestFactory().get("/.well-known/webfinger") request = RequestFactory().get("/.well-known/webfinger")
@ -23,7 +38,7 @@ class TestRFC3033WebfingerView:
response = rfc3033_webfinger_view(request) response = rfc3033_webfinger_view(request)
assert response.status_code == 400 assert response.status_code == 400
@patch("federation.hostmeta.django.generators.get_profile_func") @patch("federation.hostmeta.django.generators.get_function_from_config")
def test_unknown_handle_returns_not_found(self, mock_get_func): def test_unknown_handle_returns_not_found(self, mock_get_func):
mock_get_func.return_value = Mock(side_effect=Exception) mock_get_func.return_value = Mock(side_effect=Exception)
request = RequestFactory().get("/.well-known/webfinger?resource=acct:foobar@domain.tld") request = RequestFactory().get("/.well-known/webfinger?resource=acct:foobar@domain.tld")

Wyświetl plik

@ -5,7 +5,7 @@ import pytest
from federation.hostmeta.generators import ( from federation.hostmeta.generators import (
generate_host_meta, generate_legacy_webfinger, generate_hcard, generate_host_meta, generate_legacy_webfinger, generate_hcard,
SocialRelayWellKnown, NodeInfo, get_nodeinfo_well_known_document, RFC3033Webfinger, SocialRelayWellKnown, NodeInfo, get_nodeinfo_well_known_document, RFC3033Webfinger,
) generate_nodeinfo2_document)
from federation.tests.fixtures.payloads import DIASPORA_HOSTMETA, DIASPORA_WEBFINGER from federation.tests.fixtures.payloads import DIASPORA_HOSTMETA, DIASPORA_WEBFINGER
@ -75,6 +75,51 @@ class TestDiasporaHCardGenerator:
) )
class TestGenerateNodeInfo2Document:
def test_generate_with_minimum_data(self):
data = {
"server": {
"baseUrl": "https://example.com",
"name": "Example server",
"software": "example",
"version": "0.5.0"
},
"openRegistrations": True,
}
expected = {
"version": "1.0",
"server": {
"baseUrl": "https://example.com",
"name": "Example server",
"software": "example",
"version": "0.5.0"
},
"organization": {
"account": None,
"contact": None,
"name": None,
},
"protocols": ["diaspora"],
"relay": "",
"services": {
"inbound": [],
"outbound": []
},
"openRegistrations": True,
"usage": {
"users": {
"total": None,
"activeHalfyear": None,
"activeMonth": None,
"activeWeek": None,
},
"localPosts": None,
"localComments": None,
}
}
assert generate_nodeinfo2_document(**data) == expected
class TestSocialRelayWellKnownGenerator: class TestSocialRelayWellKnownGenerator:
def test_valid_social_relay_well_known(self): def test_valid_social_relay_well_known(self):
with open("federation/hostmeta/schemas/social-relay-well-known.json") as f: with open("federation/hostmeta/schemas/social-relay-well-known.json") as f: