Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • funkwhale/funkwhale
  • Luclu7/funkwhale
  • mbothorel/funkwhale
  • EorlBruder/funkwhale
  • tcit/funkwhale
  • JocelynDelalande/funkwhale
  • eneiluj/funkwhale
  • reg/funkwhale
  • ButterflyOfFire/funkwhale
  • m4sk1n/funkwhale
  • wxcafe/funkwhale
  • andybalaam/funkwhale
  • jcgruenhage/funkwhale
  • pblayo/funkwhale
  • joshuaboniface/funkwhale
  • n3ddy/funkwhale
  • gegeweb/funkwhale
  • tohojo/funkwhale
  • emillumine/funkwhale
  • Te-k/funkwhale
  • asaintgenis/funkwhale
  • anoadragon453/funkwhale
  • Sakada/funkwhale
  • ilianaw/funkwhale
  • l4p1n/funkwhale
  • pnizet/funkwhale
  • dante383/funkwhale
  • interfect/funkwhale
  • akhardya/funkwhale
  • svfusion/funkwhale
  • noplanman/funkwhale
  • nykopol/funkwhale
  • roipoussiere/funkwhale
  • Von/funkwhale
  • aurieh/funkwhale
  • icaria36/funkwhale
  • floreal/funkwhale
  • paulwalko/funkwhale
  • comradekingu/funkwhale
  • FurryJulie/funkwhale
  • Legolars99/funkwhale
  • Vierkantor/funkwhale
  • zachhats/funkwhale
  • heyjake/funkwhale
  • sn0w/funkwhale
  • jvoisin/funkwhale
  • gordon/funkwhale
  • Alexander/funkwhale
  • bignose/funkwhale
  • qasim.ali/funkwhale
  • fakegit/funkwhale
  • Kxze/funkwhale
  • stenstad/funkwhale
  • creak/funkwhale
  • Kaze/funkwhale
  • Tixie/funkwhale
  • IISergII/funkwhale
  • lfuelling/funkwhale
  • nhaddag/funkwhale
  • yoasif/funkwhale
  • ifischer/funkwhale
  • keslerm/funkwhale
  • flupe/funkwhale
  • petitminion/funkwhale
  • ariasuni/funkwhale
  • ollie/funkwhale
  • ngaumont/funkwhale
  • techknowlogick/funkwhale
  • Shleeble/funkwhale
  • theflyingfrog/funkwhale
  • jonatron/funkwhale
  • neobrain/funkwhale
  • eorn/funkwhale
  • KokaKiwi/funkwhale
  • u1-liquid/funkwhale
  • marzzzello/funkwhale
  • sirenwatcher/funkwhale
  • newer027/funkwhale
  • codl/funkwhale
  • Zwordi/funkwhale
  • gisforgabriel/funkwhale
  • iuriatan/funkwhale
  • simon/funkwhale
  • bheesham/funkwhale
  • zeoses/funkwhale
  • accraze/funkwhale
  • meliurwen/funkwhale
  • divadsn/funkwhale
  • Etua/funkwhale
  • sdrik/funkwhale
  • Soran/funkwhale
  • kuba-orlik/funkwhale
  • cristianvogel/funkwhale
  • Forceu/funkwhale
  • jeff/funkwhale
  • der_scheibenhacker/funkwhale
  • owlnical/funkwhale
  • jovuit/funkwhale
  • SilverFox15/funkwhale
  • phw/funkwhale
  • mayhem/funkwhale
  • sridhar/funkwhale
  • stromlin/funkwhale
  • rrrnld/funkwhale
  • nitaibezerra/funkwhale
  • jaller94/funkwhale
  • pcouy/funkwhale
  • eduxstad/funkwhale
  • codingHahn/funkwhale
  • captain/funkwhale
  • polyedre/funkwhale
  • leishenailong/funkwhale
  • ccritter/funkwhale
  • lnceballosz/funkwhale
  • fpiesche/funkwhale
  • Fanyx/funkwhale
  • markusblogde/funkwhale
  • Firobe/funkwhale
  • devilcius/funkwhale
  • freaktechnik/funkwhale
  • blopware/funkwhale
  • cone/funkwhale
  • thanksd/funkwhale
  • vachan-maker/funkwhale
  • bbenti/funkwhale
  • tarator/funkwhale
  • prplecake/funkwhale
  • DMarzal/funkwhale
  • lullis/funkwhale
  • hanacgr/funkwhale
  • albjeremias/funkwhale
  • xeruf/funkwhale
  • llelite/funkwhale
  • RoiArthurB/funkwhale
  • cloo/funkwhale
  • nztvar/funkwhale
  • Keunes/funkwhale
  • petitminion/funkwhale-petitminion
  • m-idler/funkwhale
  • SkyLeite/funkwhale
140 results
Select Git revision
Show changes
Commits on Source (3)
...@@ -19,6 +19,7 @@ class SignatureAuthentication(authentication.BaseAuthentication): ...@@ -19,6 +19,7 @@ class SignatureAuthentication(authentication.BaseAuthentication):
try: try:
actor = actors.get_actor(key_id.split("#")[0]) actor = actors.get_actor(key_id.split("#")[0])
except Exception as e: except Exception as e:
raise
raise exceptions.AuthenticationFailed(str(e)) raise exceptions.AuthenticationFailed(str(e))
if not actor.public_key: if not actor.public_key:
......
import collections
import pyld
from rest_framework import serializers
from django.core import validators
from . import ns
_cache = {}
def document_loader(url):
loader = pyld.jsonld.requests_document_loader()
doc = ns.get_by_url(url)
if doc:
return doc
if url in _cache:
return _cache[url]
resp = loader(url)
_cache[url] = resp
return resp
def expand(document):
return pyld.jsonld.expand(document)
pyld.jsonld.set_document_loader(document_loader)
class TypeField(serializers.ListField):
def __init__(self, *args, **kwargs):
kwargs.setdefault("child", serializers.URLField())
super().__init__(*args, **kwargs)
def to_internal_value(self, value):
try:
return value[0]
except IndexError:
self.fail("invalid", value)
def to_representation(self, value):
return value
class JsonLdField(serializers.ListField):
def __init__(self, *args, **kwargs):
# by default, pyld will expand all fields to list such as
# [{"@value": "something"}]
# we may not always want this behaviour
self.single = kwargs.pop("single", False)
self.value_only = kwargs.pop("value_only", False)
self.id_only = kwargs.pop("id_only", False)
self.base_field = kwargs.pop("base_field")
super().__init__(*args, **kwargs)
if self.single:
self.validators = [
v
for v in self.validators
if not isinstance(
v, (validators.MinLengthValidator, validators.MaxLengthValidator)
)
]
def to_internal_value(self, value):
self.source_attrs = [self.field_name]
value = super().to_internal_value(value)
if self.id_only:
value = [v["@id"] for v in value]
elif self.value_only:
value = [v["@value"] for v in value]
if value and self.single:
return value[0]
return value
def to_representation(self, value):
return self.base_field.to_representation(value)
class Serializer(serializers.Serializer):
jsonld_fields = []
type_source = None
id_source = None
include_id = True
include_type = True
valid_types = []
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.include_id:
self.fields["@id"] = serializers.URLField(
required=True, **{"source": self.id_source} if self.id_source else {}
)
if self.include_type:
self.fields["@type"] = TypeField(
required=True,
**{"source": self.type_source} if self.type_source else {}
)
for field_name, field, kw in self.jsonld_fields:
self.fields[field_name] = JsonLdField(
required=field.required,
base_field=field,
min_length=1 if field.required else 0,
**kw
)
def run_validation(self, initial_data):
try:
document = expand(initial_data)[0]
except IndexError:
raise serializers.ValidationError(
"Cannot parse json-ld, maybe you are missing context."
)
return super().run_validation(document)
def validate(self, validated_data):
validated_data = super().validate(validated_data)
if self.valid_types and "@type" in validated_data:
if validated_data["@type"] not in self.valid_types:
raise serializers.ValidationError(
{
"@type": "Invalid type. Allowed types are: {}".format(
", ".join(self.valid_types)
)
}
)
return validated_data
def to_representation(self, data):
data = super().to_representation(data)
compacted = pyld.jsonld.compact(data, {"@context": ns.CONTEXTS})
# While compacted repr is json-ld valid, it's probably not was is
# expected by most implementations around in the fediverse,
# so we remove the semicolons completely and the prefix
return remove_semicolons(compacted)
def remove_semicolons(struct: dict, skip=["@context"]):
"""
given a JSON-LD strut such as {
'as:inbox': 'https://:test'
},
returns {
'inbox': 'https://:test'
},
"""
new_data = collections.OrderedDict()
for k, v in struct.items():
if k in skip:
new_data[k] = v
continue
parts = k.split(":")
if len(parts) > 1:
k = ":".join(parts[1:])
if isinstance(v, dict):
v = remove_semicolons(v)
elif isinstance(v, (list, tuple)):
new_v = []
for e in v:
if isinstance(e, dict):
new_v.append(remove_semicolons(e))
else:
new_v.append(e)
v = new_v
new_data[k] = v
return new_data
...@@ -11,6 +11,9 @@ from django.utils import timezone ...@@ -11,6 +11,9 @@ from django.utils import timezone
from funkwhale_api.common import session from funkwhale_api.common import session
from funkwhale_api.music import utils as music_utils from funkwhale_api.music import utils as music_utils
from . import ns
TYPE_CHOICES = [ TYPE_CHOICES = [
("Person", "Person"), ("Person", "Person"),
("Application", "Application"), ("Application", "Application"),
...@@ -97,6 +100,21 @@ class Actor(models.Model): ...@@ -97,6 +100,21 @@ class Actor(models.Model):
follows = self.received_follows.filter(approved=True) follows = self.received_follows.filter(approved=True)
return self.followers.filter(pk__in=follows.values_list("actor", flat=True)) return self.followers.filter(pk__in=follows.values_list("actor", flat=True))
def public_key_repr(self):
return {
"owner": self.url,
"publicKeyPem": self.public_key,
"@id": "{}#main-key".format(self.url),
}
@property
def endpoints(self):
return {"sharedInbox": self.shared_inbox_url}
@property
def type_ap(self):
return ns.SHORT["as:{}".format(self.type)]
class Follow(models.Model): class Follow(models.Model):
ap_type = "Follow" ap_type = "Follow"
......
import collections
NS = {
"ldp": {
"contextUrl": None,
"documentUrl": "http://www.w3.org/ns/ldp",
"document": {
"@context": {
"ldp": "http://www.w3.org/ns/ldp#",
"id": "@id",
"type": "@type",
"Container": "ldp:Container",
"BasicContainer": "ldp:BasicContainer",
"DirectContainer": "ldp:DirectContainer",
"IndirectContainer": "ldp:IndirectContainer",
"hasMemberRelation": {"@id": "ldp:hasMemberRelation", "@type": "@id"},
"isMemberOfRelation": {"@id": "ldp:isMemberOfRelation", "@type": "@id"},
"membershipResource": {"@id": "ldp:membershipResource", "@type": "@id"},
"insertedContentRelation": {
"@id": "ldp:insertedContentRelation",
"@type": "@id",
},
"contains": {"@id": "ldp:contains", "@type": "@id"},
"member": {"@id": "ldp:member", "@type": "@id"},
"constrainedBy": {"@id": "ldp:constrainedBy", "@type": "@id"},
"Resource": "ldp:Resource",
"RDFSource": "ldp:RDFSource",
"NonRDFSource": "ldp:NonRDFSource",
"MemberSubject": "ldp:MemberSubject",
"PreferContainment": "ldp:PreferContainment",
"PreferMembership": "ldp:PreferMembership",
"PreferMinimalContainer": "ldp:PreferMinimalContainer",
"PageSortCriterion": "ldp:PageSortCriterion",
"pageSortCriteria": {
"@id": "ldp:pageSortCriteria",
"@type": "@id",
"@container": "@list",
},
"pageSortPredicate": {"@id": "ldp:pageSortPredicate", "@type": "@id"},
"pageSortOrder": {"@id": "ldp:pageSortOrder", "@type": "@id"},
"pageSortCollation": {"@id": "ldp:pageSortCollation", "@type": "@id"},
"Ascending": "ldp:Ascending",
"Descending": "ldp:Descending",
"Page": "ldp:Page",
"pageSequence": {"@id": "ldp:pageSequence", "@type": "@id"},
"inbox": {"@id": "ldp:inbox", "@type": "@id"},
}
},
},
"as": {
"contextUrl": None,
"documentUrl": "https://www.w3.org/ns/activitystreams",
"document": {
"@context": {
"source": "as:source",
"summary": "as:summary",
"target": {"@type": "@id", "@id": "as:target"},
"Block": "as:Block",
"shares": {"@type": "@id", "@id": "as:shares"},
"liked": {"@type": "@id", "@id": "as:liked"},
"location": {"@type": "@id", "@id": "as:location"},
"name": "as:name",
"endpoints": {"@type": "@id", "@id": "as:endpoints"},
"following": {"@type": "@id", "@id": "as:following"},
"Accept": "as:Accept",
"Object": "as:Object",
"instrument": {"@type": "@id", "@id": "as:instrument"},
"Mention": "as:Mention",
"context": {"@type": "@id", "@id": "as:context"},
"Travel": "as:Travel",
"Remove": "as:Remove",
"Profile": "as:Profile",
"preview": {"@type": "@id", "@id": "as:preview"},
"TentativeReject": "as:TentativeReject",
"startIndex": {
"@type": "xsd:nonNegativeInteger",
"@id": "as:startIndex",
},
"endTime": {"@type": "xsd:dateTime", "@id": "as:endTime"},
"current": {"@type": "@id", "@id": "as:current"},
"altitude": {"@type": "xsd:float", "@id": "as:altitude"},
"startTime": {"@type": "xsd:dateTime", "@id": "as:startTime"},
"CollectionPage": "as:CollectionPage",
"bto": {"@type": "@id", "@id": "as:bto"},
"orderedItems": {
"@container": "@list",
"@type": "@id",
"@id": "as:items",
},
"attributedTo": {"@type": "@id", "@id": "as:attributedTo"},
"next": {"@type": "@id", "@id": "as:next"},
"prev": {"@type": "@id", "@id": "as:prev"},
"Leave": "as:Leave",
"signClientKey": {"@type": "@id", "@id": "as:signClientKey"},
"Image": "as:Image",
"anyOf": {"@type": "@id", "@id": "as:anyOf"},
"icon": {"@type": "@id", "@id": "as:icon"},
"@vocab": "_:",
"bcc": {"@type": "@id", "@id": "as:bcc"},
"latitude": {"@type": "xsd:float", "@id": "as:latitude"},
"rel": "as:rel",
"mediaType": "as:mediaType",
"Create": "as:Create",
"Document": "as:Document",
"result": {"@type": "@id", "@id": "as:result"},
"subject": {"@type": "@id", "@id": "as:subject"},
"uploadMedia": {"@type": "@id", "@id": "as:uploadMedia"},
"oneOf": {"@type": "@id", "@id": "as:oneOf"},
"streams": {"@type": "@id", "@id": "as:streams"},
"Like": "as:Like",
"Delete": "as:Delete",
"object": {"@type": "@id", "@id": "as:object"},
"Tombstone": "as:Tombstone",
"replies": {"@type": "@id", "@id": "as:replies"},
"followers": {"@type": "@id", "@id": "as:followers"},
"inReplyTo": {"@type": "@id", "@id": "as:inReplyTo"},
"content": "as:content",
"relationship": {"@type": "@id", "@id": "as:relationship"},
"duration": {"@type": "xsd:duration", "@id": "as:duration"},
"Event": "as:Event",
"longitude": {"@type": "xsd:float", "@id": "as:longitude"},
"Organization": "as:Organization",
"contentMap": {"@container": "@language", "@id": "as:content"},
"describes": {"@type": "@id", "@id": "as:describes"},
"oauthAuthorizationEndpoint": {
"@type": "@id",
"@id": "as:oauthAuthorizationEndpoint",
},
"Update": "as:Update",
"Listen": "as:Listen",
"Ignore": "as:Ignore",
"Article": "as:Article",
"cc": {"@type": "@id", "@id": "as:cc"},
"Page": "as:Page",
"Audio": "as:Audio",
"items": {"@type": "@id", "@id": "as:items"},
"xsd": "http://www.w3.org/2001/XMLSchema#",
"Reject": "as:Reject",
"Person": "as:Person",
"nameMap": {"@container": "@language", "@id": "as:name"},
"outbox": {"@type": "@id", "@id": "as:outbox"},
"actor": {"@type": "@id", "@id": "as:actor"},
"Move": "as:Move",
"preferredUsername": "as:preferredUsername",
"IsFollowedBy": "as:IsFollowedBy",
"Follow": "as:Follow",
"origin": {"@type": "@id", "@id": "as:origin"},
"Link": "as:Link",
"units": "as:units",
"width": {"@type": "xsd:nonNegativeInteger", "@id": "as:width"},
"likes": {"@type": "@id", "@id": "as:likes"},
"IsFollowing": "as:IsFollowing",
"Public": {"@type": "@id", "@id": "as:Public"},
"Service": "as:Service",
"Note": "as:Note",
"IsContact": "as:IsContact",
"Join": "as:Join",
"Place": "as:Place",
"attachment": {"@type": "@id", "@id": "as:attachment"},
"IsMember": "as:IsMember",
"OrderedCollectionPage": "as:OrderedCollectionPage",
"Invite": "as:Invite",
"ldp": "http://www.w3.org/ns/ldp#",
"to": {"@type": "@id", "@id": "as:to"},
"Activity": "as:Activity",
"generator": {"@type": "@id", "@id": "as:generator"},
"type": "@type",
"tag": {"@type": "@id", "@id": "as:tag"},
"hreflang": "as:hreflang",
"Dislike": "as:Dislike",
"Question": "as:Question",
"radius": {"@type": "xsd:float", "@id": "as:radius"},
"closed": {"@type": "xsd:dateTime", "@id": "as:closed"},
"sharedInbox": {"@type": "@id", "@id": "as:sharedInbox"},
"View": "as:View",
"Flag": "as:Flag",
"updated": {"@type": "xsd:dateTime", "@id": "as:updated"},
"Offer": "as:Offer",
"as": "https://www.w3.org/ns/activitystreams#",
"deleted": {"@type": "xsd:dateTime", "@id": "as:deleted"},
"last": {"@type": "@id", "@id": "as:last"},
"audience": {"@type": "@id", "@id": "as:audience"},
"summaryMap": {"@container": "@language", "@id": "as:summary"},
"proxyUrl": {"@type": "@id", "@id": "as:proxyUrl"},
"IntransitiveActivity": "as:IntransitiveActivity",
"OrderedCollection": "as:OrderedCollection",
"Group": "as:Group",
"oauthTokenEndpoint": {"@type": "@id", "@id": "as:oauthTokenEndpoint"},
"Add": "as:Add",
"Video": "as:Video",
"Read": "as:Read",
"provideClientKey": {"@type": "@id", "@id": "as:provideClientKey"},
"first": {"@type": "@id", "@id": "as:first"},
"TentativeAccept": "as:TentativeAccept",
"formerType": {"@type": "@id", "@id": "as:formerType"},
"href": {"@type": "@id", "@id": "as:href"},
"Collection": "as:Collection",
"accuracy": {"@type": "xsd:float", "@id": "as:accuracy"},
"height": {"@type": "xsd:nonNegativeInteger", "@id": "as:height"},
"Announce": "as:Announce",
"Relationship": "as:Relationship",
"id": "@id",
"published": {"@type": "xsd:dateTime", "@id": "as:published"},
"url": {"@type": "@id", "@id": "as:url"},
"totalItems": {
"@type": "xsd:nonNegativeInteger",
"@id": "as:totalItems",
},
"Application": "as:Application",
"image": {"@type": "@id", "@id": "as:image"},
"Arrive": "as:Arrive",
"partOf": {"@type": "@id", "@id": "as:partOf"},
"Undo": "as:Undo",
"inbox": {"@type": "@id", "@id": "ldp:inbox"},
# extensions
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
}
},
},
"sec": {
"contextUrl": None,
"documentUrl": "https://w3id.org/security/v1",
"document": {
"@context": {
"password": "sec:password",
"salt": "sec:salt",
"sec": "https://w3id.org/security#",
"privateKeyPem": "sec:privateKeyPem",
"LinkedDataSignature2016": "sec:LinkedDataSignature2016",
"iterationCount": "sec:iterationCount",
"canonicalizationAlgorithm": "sec:canonicalizationAlgorithm",
"cipherData": "sec:cipherData",
"revoked": {"@type": "xsd:dateTime", "@id": "sec:revoked"},
"CryptographicKey": "sec:Key",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"publicKey": {"@type": "@id", "@id": "sec:publicKey"},
"domain": "sec:domain",
"signature": "sec:signature",
"authenticationTag": "sec:authenticationTag",
"dc": "http://purl.org/dc/terms/",
"nonce": "sec:nonce",
"publicKeyPem": "sec:publicKeyPem",
"owner": {"@type": "@id", "@id": "sec:owner"},
"publicKeyService": {"@type": "@id", "@id": "sec:publicKeyService"},
"expiration": {"@type": "xsd:dateTime", "@id": "sec:expiration"},
"digestAlgorithm": "sec:digestAlgorithm",
"signatureAlgorithm": "sec:signingAlgorithm",
"normalizationAlgorithm": "sec:normalizationAlgorithm",
"privateKey": {"@type": "@id", "@id": "sec:privateKey"},
"creator": {"@type": "@id", "@id": "dc:creator"},
"cipherAlgorithm": "sec:cipherAlgorithm",
"id": "@id",
"Ed25519Signature2018": "sec:Ed25519Signature2018",
"encryptionKey": "sec:encryptionKey",
"created": {"@type": "xsd:dateTime", "@id": "dc:created"},
"signatureValue": "sec:signatureValue",
"cipherKey": "sec:cipherKey",
"type": "@type",
"digestValue": "sec:digestValue",
"publicKeyBase58": "sec:publicKeyBase58",
"expires": {"@type": "xsd:dateTime", "@id": "sec:expiration"},
"EncryptedMessage": "sec:EncryptedMessage",
"EcdsaKoblitzSignature2016": "sec:EcdsaKoblitzSignature2016",
"LinkedDataSignature2015": "sec:LinkedDataSignature2015",
"GraphSignature2012": "sec:GraphSignature2012",
"initializationVector": "sec:initializationVector",
}
},
},
}
SHORT = {}
VALIDATORS = collections.defaultdict(list)
for ns, data in NS.items():
context = data["document"]["@context"]
base = context[ns]
for key, value in data["document"]["@context"].items():
if key == ns:
continue
try:
SHORT[value] = base + key
except TypeError:
if value["@id"] in SHORT:
continue
if not value["@id"].startswith("{}:".format(ns)):
# this is an alias
SHORT[":".join([ns, key])] = base + key
else:
SHORT[value["@id"]] = base + key
FULL_TO_SHORT = {v: k for k, v in SHORT.items()}
CONTEXTS = [v["documentUrl"] for k, v in NS.items()]
ADDITIONAL_CONTEXT = {"manuallyApprovesFollowers": "as:manuallyApprovesFollowers"}
CONTEXTS.append(ADDITIONAL_CONTEXT)
def get_by_url(url: str):
for v in NS.values():
if v["documentUrl"] == url:
return v
def register_validator(SHORT: list):
def decorator(validator):
for shortcut in SHORT:
VALIDATORS[shortcut].append(validator)
return validator
return decorator
...@@ -10,7 +10,7 @@ from funkwhale_api.common import utils as funkwhale_utils ...@@ -10,7 +10,7 @@ from funkwhale_api.common import utils as funkwhale_utils
from funkwhale_api.music import models as music_models from funkwhale_api.music import models as music_models
from funkwhale_api.music import tasks as music_tasks from funkwhale_api.music import tasks as music_tasks
from . import activity, filters, models, utils from . import activity, filters, models, utils, ns, jsonld
AP_CONTEXT = [ AP_CONTEXT = [
"https://www.w3.org/ns/activitystreams", "https://www.w3.org/ns/activitystreams",
...@@ -21,74 +21,135 @@ AP_CONTEXT = [ ...@@ -21,74 +21,135 @@ AP_CONTEXT = [
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ActorSerializer(serializers.Serializer): class EndpointsField(jsonld.Serializer):
id = serializers.URLField(max_length=500) include_id = False
outbox = serializers.URLField(max_length=500) include_type = False
inbox = serializers.URLField(max_length=500) jsonld_fields = [
type = serializers.ChoiceField(choices=models.TYPE_CHOICES) (
preferredUsername = serializers.CharField() ns.SHORT["as:sharedInbox"],
manuallyApprovesFollowers = serializers.NullBooleanField(required=False) serializers.URLField(),
name = serializers.CharField(required=False, max_length=200) {"source": "sharedInbox", "id_only": True, "single": True},
summary = serializers.CharField(max_length=None, required=False) )
followers = serializers.URLField(max_length=500, required=False, allow_null=True) ]
following = serializers.URLField(max_length=500, required=False, allow_null=True)
publicKey = serializers.JSONField(required=False)
def to_representation(self, instance):
ret = { class PublicKeyField(jsonld.Serializer):
"id": instance.url, include_type = False
"outbox": instance.outbox_url, jsonld_fields = [
"inbox": instance.inbox_url, (
"preferredUsername": instance.preferred_username, ns.SHORT["sec:owner"],
"type": instance.type, serializers.URLField(),
} {"source": "owner", "single": True, "id_only": True},
if instance.name: ),
ret["name"] = instance.name (
if instance.followers_url: ns.SHORT["sec:publicKeyPem"],
ret["followers"] = instance.followers_url serializers.CharField(),
if instance.following_url: {"source": "publicKeyPem", "single": True, "value_only": True},
ret["following"] = instance.following_url ),
if instance.summary: ]
ret["summary"] = instance.summary
if instance.manually_approves_followers is not None:
ret["manuallyApprovesFollowers"] = instance.manually_approves_followers class ActorSerializer(jsonld.Serializer):
id_source = "url"
ret["@context"] = AP_CONTEXT type_source = "type_ap"
if instance.public_key: valid_types = [
ret["publicKey"] = { ns.SHORT["as:Application"],
"owner": instance.url, ns.SHORT["as:Group"],
"publicKeyPem": instance.public_key, ns.SHORT["as:Organization"],
"id": "{}#main-key".format(instance.url), ns.SHORT["as:Person"],
} ns.SHORT["as:Service"],
ret["endpoints"] = {} ]
if instance.shared_inbox_url: jsonld_fields = [
ret["endpoints"]["sharedInbox"] = instance.shared_inbox_url (
return ret ns.SHORT["as:outbox"],
serializers.URLField(max_length=500),
{"source": "outbox_url", "id_only": True, "single": True},
),
(
ns.SHORT["ldp:inbox"],
serializers.URLField(max_length=500),
{"source": "inbox_url", "id_only": True, "single": True},
),
(
ns.SHORT["as:preferredUsername"],
serializers.CharField(),
{"source": "preferred_username", "value_only": True, "single": True},
),
(
ns.SHORT["as:manuallyApprovesFollowers"],
serializers.NullBooleanField(required=False),
{
"source": "manually_approves_followers",
"value_only": True,
"single": True,
},
),
(
ns.SHORT["as:name"],
serializers.CharField(required=False, max_length=200),
{"source": "name", "value_only": True, "single": True},
),
(
ns.SHORT["as:summary"],
serializers.CharField(max_length=None, required=False),
{"source": "summary", "value_only": True, "single": True},
),
(
ns.SHORT["as:followers"],
serializers.URLField(max_length=500, required=False, allow_null=True),
{"source": "followers_url", "id_only": True, "single": True},
),
(
ns.SHORT["as:following"],
serializers.URLField(max_length=500, required=False, allow_null=True),
{"source": "following_url", "id_only": True, "single": True},
),
(
ns.SHORT["sec:publicKey"],
serializers.JSONField(required=False),
{"source": "public_key_repr", "value_only": True, "single": True},
),
(
ns.SHORT["as:endpoints"],
EndpointsField(required=False),
{"source": "endpoints", "single": True},
),
(
ns.SHORT["sec:publicKey"],
PublicKeyField(required=False),
{"source": "public_key_repr", "single": True},
),
]
def prepare_missing_fields(self): def prepare_missing_fields(self):
kwargs = { kwargs = {
"url": self.validated_data["id"], "url": self.validated_data["url"],
"outbox_url": self.validated_data["outbox"], "outbox_url": self.validated_data[ns.SHORT["as:outbox"]],
"inbox_url": self.validated_data["inbox"], "inbox_url": self.validated_data[ns.SHORT["ldp:inbox"]],
"following_url": self.validated_data.get("following"), "following_url": self.validated_data.get(ns.SHORT["as:following"]),
"followers_url": self.validated_data.get("followers"), "followers_url": self.validated_data.get(ns.SHORT["as:followers"]),
"summary": self.validated_data.get("summary"), "summary": self.validated_data.get(ns.SHORT["as:summary"]),
"type": self.validated_data["type"], "type": self.validated_data["type_ap"].split("#")[-1],
"name": self.validated_data.get("name"), "name": self.validated_data.get(ns.SHORT["as:name"]),
"preferred_username": self.validated_data["preferredUsername"], "preferred_username": self.validated_data[ns.SHORT["as:preferredUsername"]],
"manually_approves_followers": self.validated_data.get(
ns.SHORT["as:manuallyApprovesFollowers"]
),
} }
maf = self.validated_data.get("manuallyApprovesFollowers")
if maf is not None:
kwargs["manually_approves_followers"] = maf
domain = urllib.parse.urlparse(kwargs["url"]).netloc domain = urllib.parse.urlparse(kwargs["url"]).netloc
kwargs["domain"] = domain kwargs["domain"] = domain
for endpoint, url in self.initial_data.get("endpoints", {}).items(): for endpoint, url in self.validated_data.get(
if endpoint == "sharedInbox": ns.SHORT["as:endpoints"], {}
kwargs["shared_inbox_url"] = url ).items():
if endpoint == ns.SHORT["as:sharedInbox"]:
kwargs["shared_inbox_url"] = url[0]["@id"]
break break
public_key_data = self.validated_data.get(ns.SHORT["sec:publicKey"], {})
try: try:
kwargs["public_key"] = self.initial_data["publicKey"]["publicKeyPem"] kwargs["public_key"] = public_key_data[ns.SHORT["sec:publicKeyPem"]][0][
except KeyError: "@value"
]
except (KeyError, IndexError):
pass pass
return kwargs return kwargs
...@@ -124,22 +185,22 @@ class APIActorSerializer(serializers.ModelSerializer): ...@@ -124,22 +185,22 @@ class APIActorSerializer(serializers.ModelSerializer):
class LibraryActorSerializer(ActorSerializer): class LibraryActorSerializer(ActorSerializer):
url = serializers.ListField(child=serializers.JSONField()) # url = serializers.ListField(child=serializers.JSONField())
jsonld_fields = ActorSerializer.jsonld_fields + [
(
ns.SHORT["as:url"],
jsonld.Serializer(required=True),
{"source": "library_url"},
)
]
def validate(self, validated_data): def validate(self, validated_data):
try: validated_data = super().validate(validated_data)
urls = validated_data["url"] for url in validated_data[ns.SHORT["as:url"]]:
except KeyError: if url[ns.SHORT["as:name"]][0]["@value"] != "library":
raise serializers.ValidationError("Missing URL field")
for u in urls:
try:
if u["name"] != "library":
continue continue
validated_data["library_url"] = u["href"] validated_data["library_url"] = url[ns.SHORT["as:href"]][0]["@id"]
break break
except KeyError:
continue
return validated_data return validated_data
......
...@@ -66,3 +66,4 @@ cryptography>=2,<3 ...@@ -66,3 +66,4 @@ cryptography>=2,<3
# clone until the branch is merged and released upstream # clone until the branch is merged and released upstream
git+https://github.com/EliotBerriot/requests-http-signature.git@signature-header-support git+https://github.com/EliotBerriot/requests-http-signature.git@signature-header-support
django-cleanup==2.1.0 django-cleanup==2.1.0
pyld>=1,<1.1
from funkwhale_api.federation import authentication, keys from funkwhale_api.federation import authentication, keys, ns
def test_authenticate(factories, mocker, api_request): def test_authenticate(factories, mocker, api_request):
...@@ -7,10 +7,13 @@ def test_authenticate(factories, mocker, api_request): ...@@ -7,10 +7,13 @@ def test_authenticate(factories, mocker, api_request):
mocker.patch( mocker.patch(
"funkwhale_api.federation.actors.get_actor_data", "funkwhale_api.federation.actors.get_actor_data",
return_value={ return_value={
"@context": ns.CONTEXTS,
"id": actor_url, "id": actor_url,
"type": "Person", "type": "Person",
"outbox": "https://test.com", "outbox": "https://test.com",
"inbox": "https://test.com", "inbox": "https://test.com",
"following": "https://test.com",
"followers": "https://test.com",
"preferredUsername": "test", "preferredUsername": "test",
"publicKey": { "publicKey": {
"publicKeyPem": public.decode("utf-8"), "publicKeyPem": public.decode("utf-8"),
......
import pytest
from rest_framework import serializers
from funkwhale_api.federation import jsonld
from funkwhale_api.federation import ns
def test_expand(requests_mock):
document = {
"@context": ns.NS["as"]["documentUrl"],
"type": "Person",
"summary": "Hello world",
"id": "http://test.document",
}
expanded = jsonld.expand(document)
assert expanded == [
{
"@type": [ns.SHORT["as:Person"]],
ns.SHORT["as:summary"]: [{"@value": "Hello world"}],
"@id": document["id"],
}
]
def test_register_validator():
def validator(value):
pass
ns.register_validator(["as:test", "hello:world"])(validator)
assert ns.VALIDATORS["as:test"] == [validator]
assert ns.VALIDATORS["hello:world"] == [validator]
@pytest.mark.parametrize(
"field_conf,expected",
[
({}, [{"@value": "Hello world"}]),
({"single": True}, {"@value": "Hello world"}),
({"value_only": True}, ["Hello world"]),
({"value_only": True, "single": True}, "Hello world"),
],
)
def test_json_ld_serializer_validation_value(field_conf, expected):
document = {
"@context": ns.NS["as"]["documentUrl"],
"type": "Person",
"summary": "Hello world",
"id": "http://test.document",
}
class Serializer(jsonld.Serializer):
jsonld_fields = [(ns.SHORT["as:summary"], serializers.CharField(), field_conf)]
serializer = Serializer(data=document)
assert serializer.is_valid(raise_exception=True)
assert serializer.validated_data == {
"@type": ns.SHORT["as:Person"],
ns.SHORT["as:summary"]: expected,
"@id": document["id"],
}
@pytest.mark.parametrize(
"field_conf,expected",
[
({}, [{"@id": "http://test"}]),
({"single": True}, {"@id": "http://test"}),
({"id_only": True}, ["http://test"]),
({"id_only": True, "single": True}, "http://test"),
],
)
def test_json_ld_serializer_validation_id(field_conf, expected):
document = {
"@context": ns.NS["as"]["documentUrl"],
"type": "Person",
"outbox": "http://test",
"id": "http://test.document",
}
class Serializer(jsonld.Serializer):
jsonld_fields = [(ns.SHORT["as:outbox"], serializers.URLField(), field_conf)]
serializer = Serializer(data=document)
assert serializer.is_valid(raise_exception=True)
assert serializer.validated_data == {
"@type": ns.SHORT["as:Person"],
ns.SHORT["as:outbox"]: expected,
"@id": document["id"],
}
def test_json_ld_validates_type_failing():
document = {
"@context": ns.NS["as"]["documentUrl"],
"type": "Disallowed",
"outbox": "http://test",
"id": "http://test.document",
}
class Serializer(jsonld.Serializer):
valid_types = [ns.SHORT["as:Person"]]
serializer = Serializer(data=document)
assert serializer.is_valid() is False
assert "@type" in serializer.errors
def test_json_ld_validates_type_success():
document = {
"@context": ns.NS["as"]["documentUrl"],
"type": "Person",
"outbox": "http://test",
"id": "http://test.document",
}
class Serializer(jsonld.Serializer):
valid_types = [ns.SHORT["as:Person"]]
serializer = Serializer(data=document)
assert serializer.is_valid(raise_exception=True) is True
def test_json_ld_serializer_to_representation():
class Serializer(jsonld.Serializer):
jsonld_fields = [
(ns.SHORT["as:summary"], serializers.CharField(), {"source": "my_summary"})
]
type_source = "my_type_field"
id_source = "my_id_field"
serializer = Serializer(
{
"my_type_field": ns.SHORT["as:Person"],
"my_id_field": "http://hello.world",
"my_summary": "Hello",
}
)
expected = {
"type": "Person",
"summary": "Hello",
"id": "http://hello.world",
"@context": [
"http://www.w3.org/ns/ldp",
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
ns.ADDITIONAL_CONTEXT,
],
}
assert serializer.data == expected
def test_remove_semicolon():
struct = {
"@context": {"this:is": "skipped"},
"as:inbox": "https://test1",
"some:other": {"prop:erty": ["hello", {"wo:rld": "hi"}]},
}
expected = {
"@context": {"this:is": "skipped"},
"inbox": "https://test1",
"other": {"erty": ["hello", {"rld": "hi"}]},
}
result = jsonld.remove_semicolons(struct)
assert result == expected
...@@ -2,11 +2,12 @@ import arrow ...@@ -2,11 +2,12 @@ import arrow
import pytest import pytest
from django.core.paginator import Paginator from django.core.paginator import Paginator
from funkwhale_api.federation import actors, models, serializers, utils from funkwhale_api.federation import actors, ns, models, serializers, utils
def test_actor_serializer_from_ap(db): def test_actor_serializer_from_ap(db):
payload = { payload = {
"@context": ns.CONTEXTS,
"id": "https://test.federation/user", "id": "https://test.federation/user",
"type": "Person", "type": "Person",
"following": "https://test.federation/user/following", "following": "https://test.federation/user/following",
...@@ -48,6 +49,7 @@ def test_actor_serializer_from_ap(db): ...@@ -48,6 +49,7 @@ def test_actor_serializer_from_ap(db):
def test_actor_serializer_only_mandatory_field_from_ap(db): def test_actor_serializer_only_mandatory_field_from_ap(db):
payload = { payload = {
"@context": ns.CONTEXTS,
"id": "https://test.federation/user", "id": "https://test.federation/user",
"type": "Person", "type": "Person",
"following": "https://test.federation/user/following", "following": "https://test.federation/user/following",
...@@ -75,11 +77,7 @@ def test_actor_serializer_only_mandatory_field_from_ap(db): ...@@ -75,11 +77,7 @@ def test_actor_serializer_only_mandatory_field_from_ap(db):
def test_actor_serializer_to_ap(): def test_actor_serializer_to_ap():
expected = { expected = {
"@context": [ "@context": ns.CONTEXTS,
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{},
],
"id": "https://test.federation/user", "id": "https://test.federation/user",
"type": "Person", "type": "Person",
"following": "https://test.federation/user/following", "following": "https://test.federation/user/following",
......