kopia lustrzana https://gitlab.com/jaywink/federation
Merge pull request #123 from jaywink/nodeinfo2
Add NodeInfo2 generator and Django viewmerge-requests/130/head
commit
3bf19d2a8b
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"),
|
||||
]
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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:
|
||||
|
|
Ładowanie…
Reference in New Issue