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 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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Ładowanie…
Reference in New Issue