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
### Backwards incompatible changes

Wyświetl plik

@ -1,8 +1,12 @@
# -*- coding: utf-8 -*-
import warnings
from base64 import b64encode
import json
import os
from string import Template
from jsonschema import validate
from jsonschema.exceptions import ValidationError
from xrd import XRD, Link, Element
@ -214,3 +218,47 @@ class SocialRelayWellKnown(object):
with open(schema_path) as f:
schema = json.load(f)
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
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"?>
<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")
with pytest.raises(ValidationError):
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)