kopia lustrzana https://gitlab.com/jaywink/federation
Add Accept entity and ActivityPub follow auto-Accept flow
When receiving an ActivityPub Follow, send back an Accept activity automatically. Due to application hook needed to fetch sending user private key (for signing), this is only available if Django is installed since currently application hooks exist only for Django configuration. Django applications should include a new configuration item "get_private_key_function" which points to a function which takes a user identifier (fid, handle or guid) and returns a private key in RSA object format.merge-requests/143/head
rodzic
d8a067a691
commit
353ae9ff9d
|
@ -118,6 +118,7 @@ Some settings need to be set in Django settings. An example is below:
|
|||
FEDERATION = {
|
||||
"base_url": "https://myserver.domain.tld,
|
||||
"get_object_function": "myproject.utils.get_object",
|
||||
"get_private_key_function": "myproject.utils.get_private_key",
|
||||
"get_profile_function": "myproject.utils.get_profile",
|
||||
"nodeinfo2_function": "myproject.utils.get_nodeinfo2_data",
|
||||
"process_payload_function": "myproject.utils.process_payload",
|
||||
|
@ -126,6 +127,7 @@ Some settings need to be set in Django settings. An example is below:
|
|||
|
||||
* ``base_url`` is the base URL of the server, ie protocol://domain.tld.
|
||||
* ``get_object_function`` should be the full path to a function that will return the object matching the ActivityPub ID for the request object passed to this function.
|
||||
* ``get_private_key_function`` should be the full path to a function that will accept a federation ID (url, handle or guid) and return the private key of the user (as an RSA object). Required for example to sign outbound messages in some cases.
|
||||
* ``get_profile_function`` should be the full path to a function that should return a ``Profile`` entity. The function should take the following parameters: ``handle``, ``guid`` and ``request``. It should look up a profile with one or more of the provided parameters.
|
||||
* ``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:
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import logging
|
||||
import uuid
|
||||
from typing import Dict
|
||||
|
||||
from federation.entities.activitypub.constants import (
|
||||
|
@ -5,13 +7,57 @@ from federation.entities.activitypub.constants import (
|
|||
CONTEXT_LD_SIGNATURES)
|
||||
from federation.entities.activitypub.enums import ActorType, ObjectType, ActivityType
|
||||
from federation.entities.activitypub.mixins import ActivitypubEntityMixin
|
||||
from federation.entities.base import Profile, Post, Follow
|
||||
from federation.entities.base import Profile, Post, Follow, Accept
|
||||
from federation.outbound import handle_send
|
||||
from federation.types import UserType
|
||||
from federation.utils.text import with_slash
|
||||
|
||||
logger = logging.getLogger("federation")
|
||||
|
||||
|
||||
class ActivitypubAccept(ActivitypubEntityMixin, Accept):
|
||||
_type = ActivityType.ACCEPT.value
|
||||
|
||||
def to_as2(self) -> Dict:
|
||||
as2 = {
|
||||
"@context": CONTEXTS_DEFAULT,
|
||||
"id": self.activity_id,
|
||||
"type": self._type,
|
||||
"actor": self.actor_id,
|
||||
"object": self.target_id,
|
||||
}
|
||||
return as2
|
||||
|
||||
|
||||
class ActivitypubFollow(ActivitypubEntityMixin, Follow):
|
||||
_type = ActivityType.FOLLOW.value
|
||||
|
||||
def post_receive(self, attributes: Dict) -> None:
|
||||
"""
|
||||
Post receive hook - send back follow ack.
|
||||
"""
|
||||
try:
|
||||
from federation.utils.django import get_function_from_config
|
||||
except ImportError:
|
||||
logger.warning("ActivitypubFollow.post_receive - Unable to send automatic Accept back, only supported on "
|
||||
"Django currently")
|
||||
return
|
||||
get_private_key_function = get_function_from_config("get_private_key_function")
|
||||
key = get_private_key_function(self.target_id)
|
||||
if not key:
|
||||
logger.warning("ActivitypubFollow.post_receive - Failed to send automatic Accept back: could not find "
|
||||
"profile to sign it with")
|
||||
return
|
||||
accept = ActivitypubAccept(
|
||||
activity_id=f"{self.target_id}#accept-{uuid.uuid4()}",
|
||||
actor_id=self.target_id,
|
||||
target_id=self.activity_id,
|
||||
)
|
||||
try:
|
||||
handle_send(accept, UserType(id=self.target_id, private_key=key), recipients=[self.actor_id])
|
||||
except Exception:
|
||||
logger.exception("ActivitypubFollow.post_receive - Failed to send Accept back")
|
||||
|
||||
def to_as2(self) -> Dict:
|
||||
as2 = {
|
||||
"@context": CONTEXTS_DEFAULT,
|
||||
|
|
|
@ -8,6 +8,7 @@ class EnumBase(Enum):
|
|||
|
||||
|
||||
class ActivityType(EnumBase):
|
||||
ACCEPT = "Accept"
|
||||
CREATE = "Create"
|
||||
DELETE = "Delete"
|
||||
FOLLOW = "Follow"
|
||||
|
|
|
@ -23,7 +23,12 @@ def element_to_objects(
|
|||
return []
|
||||
|
||||
transformed = transform_attributes(payload, cls)
|
||||
entities.append(cls(**transformed))
|
||||
entity = cls(**transformed)
|
||||
|
||||
if hasattr(entity, "post_receive"):
|
||||
entity.post_receive(transformed)
|
||||
|
||||
entities.append(entity)
|
||||
|
||||
return entities
|
||||
|
||||
|
|
|
@ -5,6 +5,11 @@ from federation.entities.mixins import (
|
|||
EntityTypeMixin, ProviderDisplayNameMixin)
|
||||
|
||||
|
||||
class Accept(CreatedAtMixin, TargetIDMixin):
|
||||
"""An acceptance message for some target."""
|
||||
pass
|
||||
|
||||
|
||||
class Image(PublicMixin, OptionalRawContentMixin, CreatedAtMixin):
|
||||
"""Reflects a single image, possibly linked to another object."""
|
||||
remote_path = ""
|
||||
|
|
|
@ -5,6 +5,7 @@ INSTALLED_APPS = tuple()
|
|||
FEDERATION = {
|
||||
"base_url": "https://example.com",
|
||||
"get_object_function": "federation.tests.django.utils.get_object_function",
|
||||
"get_private_key_function": "federation.tests.django.utils.get_private_key",
|
||||
"get_profile_function": "federation.tests.django.utils.get_profile",
|
||||
"process_payload_function": "federation.tests.django.utils.process_payload",
|
||||
"search_path": "/search?q=",
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
from Crypto.PublicKey.RSA import RsaKey
|
||||
|
||||
from federation.entities.base import Profile
|
||||
from federation.tests.fixtures.keys import get_dummy_private_key
|
||||
|
||||
|
||||
def dummy_profile():
|
||||
|
@ -16,6 +19,10 @@ def get_object_function(object_id):
|
|||
return dummy_profile()
|
||||
|
||||
|
||||
def get_private_key(identifier: str) -> RsaKey:
|
||||
return get_dummy_private_key()
|
||||
|
||||
|
||||
def get_profile(handle=None, request=None):
|
||||
return dummy_profile()
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ def get_configuration():
|
|||
}
|
||||
configuration.update(settings.FEDERATION)
|
||||
if not all([
|
||||
"get_private_key_function" in configuration,
|
||||
"get_profile_function" in configuration,
|
||||
"base_url" in configuration,
|
||||
]):
|
||||
|
|
Ładowanie…
Reference in New Issue