Merge pull request #13 from jaywink/nodeinfo

Add support for NodeInfo document generation
merge-requests/130/head
Jason Robinson 2016-04-12 23:22:46 +03:00
commit fd1f5385d3
4 zmienionych plików z 311 dodań i 1 usunięć

Wyświetl plik

@ -1,3 +1,8 @@
## [unreleased]
### Added
- Support for generating [NodeInfo](http://nodeinfo.diaspora.software) documents using the generator `federation.hostmeta.generators.NodeInfo`. Strict validation is skipped by default, but can be enabled by passing in `raise_on_validate` to the `NodeInfo` class. By default a warning will be generated on documents that don't conform with the strict NodeInfo values. This can be disabled by passing in `skip_validate` to the class.
## [0.2.0] - 2016-04-09 ## [0.2.0] - 2016-04-09
### Backwards incompatible changes ### Backwards incompatible changes

Wyświetl plik

@ -1,8 +1,12 @@
# -*- coding: utf-8 -*-
import warnings
from base64 import b64encode from base64 import b64encode
import json import json
import os import os
from string import Template from string import Template
from jsonschema import validate from jsonschema import validate
from jsonschema.exceptions import ValidationError
from xrd import XRD, Link, Element from xrd import XRD, Link, Element
@ -214,3 +218,47 @@ class SocialRelayWellKnown(object):
with open(schema_path) as f: with open(schema_path) as f:
schema = json.load(f) schema = json.load(f)
validate(self.doc, schema) validate(self.doc, schema)
class NodeInfo(object):
"""Generate a NodeInfo document.
See spec: http://nodeinfo.diaspora.software
NodeInfo is unnecessarely restrictive in field values. We wont be supporting such strictness, though
we will raise a warning unless validation is skipped with `skip_validate=True`.
For strictness, `raise_on_validate=True` will cause a `ValidationError` to be raised.
See schema document `federation/hostmeta/schemas/nodeinfo-1.0.json` for how to instantiate this class.
"""
def __init__(self, software, protocols, services, open_registrations, usage, metadata, skip_validate=False,
raise_on_validate=False):
self.doc = {
"version": "1.0",
"software": software,
"protocols": protocols,
"services": services,
"openRegistrations": open_registrations,
"usage": usage,
"metadata": metadata,
}
self.skip_validate = skip_validate
self.raise_on_validate = raise_on_validate
def render(self):
if not self.skip_validate:
self.validate_doc()
return json.dumps(self.doc)
def validate_doc(self):
schema_path = os.path.join(os.path.dirname(__file__), "schemas", "nodeinfo-1.0.json")
with open(schema_path) as f:
schema = json.load(f)
try:
validate(self.doc, schema)
except ValidationError:
if self.raise_on_validate:
raise
warnings.warn("NodeInfo document generated does not validate against NodeInfo 1.0 specification.")

Wyświetl plik

@ -0,0 +1,206 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://nodeinfo.diaspora.software/ns/schema/1.0#",
"description": "NodeInfo schema version 1.0.",
"type": "object",
"additionalProperties": false,
"required": [
"version",
"software",
"protocols",
"services",
"openRegistrations",
"usage",
"metadata"
],
"properties": {
"version": {
"description": "The schema version, must be 1.0.",
"enum": [
"1.0"
]
},
"software": {
"description": "Metadata about server software in use.",
"type": "object",
"additionalProperties": false,
"required": [
"name",
"version"
],
"properties": {
"name": {
"description": "The canonical name of this server software.",
"enum": [
"diaspora",
"friendica",
"redmatrix"
]
},
"version": {
"description": "The version of this server software.",
"type": "string"
}
}
},
"protocols": {
"description": "The protocols supported on this server.",
"type": "object",
"additionalProperties": false,
"required": [
"inbound",
"outbound"
],
"properties": {
"inbound": {
"description": "The protocols this server can receive traffic for.",
"type": "array",
"minItems": 1,
"items": {
"enum": [
"buddycloud",
"diaspora",
"friendica",
"gnusocial",
"libertree",
"mediagoblin",
"pumpio",
"redmatrix",
"smtp",
"tent"
]
}
},
"outbound": {
"description": "The protocols this server can generate traffic for.",
"type": "array",
"minItems": 1,
"items": {
"enum": [
"buddycloud",
"diaspora",
"friendica",
"gnusocial",
"libertree",
"mediagoblin",
"pumpio",
"redmatrix",
"smtp",
"tent"
]
}
}
}
},
"services": {
"description": "The third party sites this server can connect to via their application API.",
"type": "object",
"additionalProperties": false,
"required": [
"inbound",
"outbound"
],
"properties": {
"inbound": {
"description": "The third party sites this server can retrieve messages from for combined display with regular traffic.",
"type": "array",
"minItems": 0,
"items": {
"enum": [
"appnet",
"gnusocial",
"pumpio"
]
}
},
"outbound": {
"description": "The third party sites this server can publish messages to on the behalf of a user.",
"type": "array",
"minItems": 0,
"items": {
"enum": [
"appnet",
"blogger",
"buddycloud",
"diaspora",
"dreamwidth",
"drupal",
"facebook",
"friendica",
"gnusocial",
"google",
"insanejournal",
"libertree",
"linkedin",
"livejournal",
"mediagoblin",
"myspace",
"pinterest",
"posterous",
"pumpio",
"redmatrix",
"smtp",
"tent",
"tumblr",
"twitter",
"wordpress",
"xmpp"
]
}
}
}
},
"openRegistrations": {
"description": "Whether this server allows open self-registration.",
"type": "boolean"
},
"usage": {
"description": "Usage statistics for this server.",
"type": "object",
"additionalProperties": false,
"required": [
"users"
],
"properties": {
"users": {
"description": "statistics about the users of this server.",
"type": "object",
"additionalProperties": false,
"properties": {
"total": {
"description": "The total amount of on this server registered users.",
"type": "integer",
"minimum": 0
},
"activeHalfyear": {
"description": "The amount of users that signed in at least once in the last 180 days.",
"type": "integer",
"minimum": 0
},
"activeMonth": {
"description": "The amount of users that signed in at least once in the last 30 days.",
"type": "integer",
"minimum": 0
}
}
},
"localPosts": {
"description": "The amount of posts that were made by users that are registered on this server.",
"type": "integer",
"minimum": 0
},
"localComments": {
"description": "The amount of comments that were made by users that are registered on this server.",
"type": "integer",
"minimum": 0
}
}
},
"metadata": {
"description": "Free form key value pairs for software specific values. Clients should not rely on any specific key present.",
"type": "object",
"minProperties": 0,
"additionalProperties": true
}
}
}

Wyświetl plik

@ -4,7 +4,7 @@ from jsonschema import validate, ValidationError
import pytest import pytest
from federation.hostmeta.generators import generate_host_meta, generate_legacy_webfinger, generate_hcard, \ from federation.hostmeta.generators import generate_host_meta, generate_legacy_webfinger, generate_hcard, \
SocialRelayWellKnown SocialRelayWellKnown, NodeInfo
DIASPORA_HOSTMETA = """<?xml version="1.0" encoding="UTF-8"?> DIASPORA_HOSTMETA = """<?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"> <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
@ -138,3 +138,54 @@ class TestSocialRelayWellKnownGenerator(object):
well_known = SocialRelayWellKnown(subscribe=True, tags=("foo", "bar"), scope="cities") well_known = SocialRelayWellKnown(subscribe=True, tags=("foo", "bar"), scope="cities")
with pytest.raises(ValidationError): with pytest.raises(ValidationError):
well_known.render() well_known.render()
class TestNodeInfoGenerator(object):
def _valid_nodeinfo(self, raise_on_validate=False):
return NodeInfo(
software={"name": "diaspora", "version": "0.5.4.3"},
protocols={"inbound": ["diaspora"], "outbound": ["diaspora"]},
services={"inbound": ["pumpio"], "outbound": ["twitter"]},
open_registrations=True,
usage={"users": {}},
metadata={},
raise_on_validate=raise_on_validate
)
def _invalid_nodeinfo(self, raise_on_validate=False):
return NodeInfo(
software={"name": "diaspora", "version": "0.5.4.3", "what_is_this_evil_key_here": True},
protocols={"inbound": ["diaspora"], "outbound": ["diaspora"]},
services={"inbound": ["pumpio"], "outbound": ["twitter"]},
open_registrations=True,
usage={"users": {}},
metadata={},
raise_on_validate=raise_on_validate
)
def test_nodeinfo_generator(self):
nodeinfo = self._valid_nodeinfo()
assert nodeinfo.doc["version"] == "1.0"
assert nodeinfo.doc["software"] == {"name": "diaspora", "version": "0.5.4.3"}
assert nodeinfo.doc["protocols"] == {"inbound": ["diaspora"], "outbound": ["diaspora"]}
assert nodeinfo.doc["services"] == {"inbound": ["pumpio"], "outbound": ["twitter"]}
assert nodeinfo.doc["openRegistrations"] == True
assert nodeinfo.doc["usage"] == {"users": {}}
assert nodeinfo.doc["metadata"] == {}
def test_nodeinfo_generator_raises_on_invalid_nodeinfo_and_raise_on_validate(self):
nodeinfo = self._invalid_nodeinfo(raise_on_validate=True)
with pytest.raises(ValidationError):
nodeinfo.render()
def test_nodeinfo_generator_does_not_raise_on_invalid_nodeinfo(self):
nodeinfo = self._invalid_nodeinfo()
nodeinfo.render()
def test_nodeinfo_generator_does_not_raise_on_valid_nodeinfo_and_raise_on_validate(self):
nodeinfo = self._valid_nodeinfo(raise_on_validate=True)
nodeinfo.render()
def test_nodeinfo_generator_render_returns_a_document(self):
nodeinfo = self._valid_nodeinfo()
assert isinstance(nodeinfo.render(), str)