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 NodeInfo2 generator and Django view. See documentation for details. ([related issue](https://github.com/jaywink/federation/issues/32))
### Changed
* 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_legacy_webfinger
.. autofunction:: federation.hostmeta.generators.generate_hcard
.. autofunction:: federation.hostmeta.generators.generate_nodeinfo2_document
.. autofunction:: federation.hostmeta.generators.get_nodeinfo_well_known_document
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.
.. autofunction:: federation.hostmeta.django.generators.rfc3033_webfinger_view
.. autofunction:: federation.hostmeta.django.generators.nodeinfo2_view
Configuration
.............
@ -115,6 +117,7 @@ Some settings need to be set in Django settings. An example is below:
FEDERATION = {
"base_url": "https://myserver.domain.tld,
"get_profile_function": "myproject.utils.get_profile_by_handle",
"nodeinfo2_function": "myproject.utils.get_nodeinfo2_data",
"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.
* ``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="
Protocols
@ -154,9 +169,9 @@ Diaspora
.. autofunction:: federation.utils.diaspora.parse_profile_diaspora_id
.. autofunction:: federation.utils.diaspora.parse_profile_from_hcard
.. 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_diaspora_hcard
.. autofunction:: federation.utils.diaspora.retrieve_diaspora_webfinger
.. autofunction:: federation.utils.diaspora.retrieve_diaspora_host_meta
Network

Wyświetl plik

@ -5,7 +5,7 @@ from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
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")
@ -16,6 +16,7 @@ def get_configuration():
"""
configuration = {
"hcard_path": "/hcard/users/",
"nodeinfo2_function": None,
"search_path": None,
}
configuration.update(settings.FEDERATION)
@ -27,16 +28,26 @@ def get_configuration():
return configuration
def get_profile_func():
def get_function_from_config(item):
"""
Import the function to get profile by handle.
"""
config = get_configuration()
profile_func_path = config.get("get_profile_function")
module_path, func_name = profile_func_path.rsplit(".", 1)
func_path = config.get(item)
module_path, func_name = func_path.rsplit(".", 1)
module = importlib.import_module(module_path)
profile_func = getattr(module, func_name)
return profile_func
func = getattr(module, func_name)
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):
@ -50,7 +61,7 @@ def rfc3033_webfinger_view(request, *args, **kwargs):
return HttpResponseBadRequest("Invalid resource")
handle = resource.replace("acct:", "")
profile_func = get_profile_func()
profile_func = get_function_from_config("get_profile_function")
try:
profile = profile_func(handle)

Wyświetl plik

@ -1,7 +1,9 @@
from django.conf.urls import url
from federation.hostmeta.django import rfc3033_webfinger_view
from federation.hostmeta.django.generators import nodeinfo2_view
urlpatterns = [
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()
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):
"""Generate a hCard document.

Wyświetl plik

@ -4,14 +4,29 @@ from unittest.mock import patch, Mock
from django.test import RequestFactory
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():
func = get_profile_func()
def test_get_function_from_config():
func = get_function_from_config("get_profile_function")
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:
def test_no_resource_returns_bad_request(self):
request = RequestFactory().get("/.well-known/webfinger")
@ -23,7 +38,7 @@ class TestRFC3033WebfingerView:
response = rfc3033_webfinger_view(request)
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):
mock_get_func.return_value = Mock(side_effect=Exception)
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 (
generate_host_meta, generate_legacy_webfinger, generate_hcard,
SocialRelayWellKnown, NodeInfo, get_nodeinfo_well_known_document, RFC3033Webfinger,
)
generate_nodeinfo2_document)
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:
def test_valid_social_relay_well_known(self):
with open("federation/hostmeta/schemas/social-relay-well-known.json") as f: