2019-05-12 15:32:41 +00:00
|
|
|
import logging
|
2019-03-17 14:09:34 +00:00
|
|
|
from typing import List, Callable, Dict, Union
|
2018-10-11 17:40:23 +00:00
|
|
|
|
2019-05-12 20:30:35 +00:00
|
|
|
from federation.entities.activitypub.constants import NAMESPACE_PUBLIC
|
2019-05-12 17:17:40 +00:00
|
|
|
from federation.entities.activitypub.entities import (
|
2019-07-20 22:29:58 +00:00
|
|
|
ActivitypubFollow, ActivitypubProfile, ActivitypubAccept, ActivitypubPost, ActivitypubComment,
|
|
|
|
ActivitypubRetraction)
|
|
|
|
from federation.entities.base import Follow, Profile, Accept, Post, Comment, Retraction
|
2019-03-03 01:09:25 +00:00
|
|
|
from federation.entities.mixins import BaseEntity
|
2018-10-11 17:40:23 +00:00
|
|
|
from federation.types import UserType
|
|
|
|
|
2019-05-12 15:32:41 +00:00
|
|
|
logger = logging.getLogger("federation")
|
|
|
|
|
|
|
|
|
2018-10-11 17:40:23 +00:00
|
|
|
MAPPINGS = {
|
2019-03-17 01:42:42 +00:00
|
|
|
"Accept": ActivitypubAccept,
|
2019-05-12 17:17:40 +00:00
|
|
|
"Article": ActivitypubPost,
|
2019-07-20 22:29:58 +00:00
|
|
|
"Delete": ActivitypubRetraction,
|
2019-06-28 21:55:23 +00:00
|
|
|
"Follow": ActivitypubFollow, # Technically not correct, but for now we support only following profiles
|
2019-05-12 17:17:40 +00:00
|
|
|
"Note": ActivitypubPost,
|
|
|
|
"Page": ActivitypubPost,
|
2018-10-28 21:31:33 +00:00
|
|
|
"Person": ActivitypubProfile,
|
2019-06-28 21:55:23 +00:00
|
|
|
"Undo": ActivitypubFollow, # Technically not correct, but for now we support only undoing a follow of a profile
|
2018-10-11 17:40:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-03-17 21:54:06 +00:00
|
|
|
def element_to_objects(payload: Dict) -> List:
|
2018-10-11 17:40:23 +00:00
|
|
|
"""
|
|
|
|
Transform an Element to a list of entities recursively.
|
|
|
|
"""
|
|
|
|
entities = []
|
2019-07-20 22:29:58 +00:00
|
|
|
if payload.get('type') == "Delete":
|
|
|
|
cls = ActivitypubRetraction
|
|
|
|
elif isinstance(payload.get('object'), dict) and payload["object"].get('type'):
|
2019-06-29 22:02:27 +00:00
|
|
|
if payload["object"]["type"] == "Note" and payload["object"].get("inReplyTo"):
|
|
|
|
cls = ActivitypubComment
|
|
|
|
else:
|
|
|
|
cls = MAPPINGS.get(payload["object"]["type"])
|
2019-05-12 18:57:44 +00:00
|
|
|
else:
|
2019-06-29 22:02:27 +00:00
|
|
|
cls = MAPPINGS.get(payload.get('type'))
|
2018-10-11 17:40:23 +00:00
|
|
|
if not cls:
|
|
|
|
return []
|
|
|
|
|
|
|
|
transformed = transform_attributes(payload, cls)
|
2019-03-17 01:17:10 +00:00
|
|
|
entity = cls(**transformed)
|
2019-05-12 15:32:41 +00:00
|
|
|
# Add protocol name
|
|
|
|
entity._source_protocol = "activitypub"
|
|
|
|
# Save element object to entity for possible later use
|
|
|
|
entity._source_object = payload
|
2019-03-17 01:17:10 +00:00
|
|
|
|
|
|
|
if hasattr(entity, "post_receive"):
|
2019-03-17 21:54:06 +00:00
|
|
|
entity.post_receive()
|
2019-03-17 01:17:10 +00:00
|
|
|
|
2019-05-12 15:32:41 +00:00
|
|
|
try:
|
|
|
|
entity.validate()
|
|
|
|
except ValueError as ex:
|
|
|
|
logger.error("Failed to validate entity %s: %s", entity, ex, extra={
|
|
|
|
"transformed": transformed,
|
|
|
|
})
|
|
|
|
return []
|
|
|
|
# Extract mentions
|
|
|
|
entity._mentions = entity.extract_mentions()
|
|
|
|
|
2019-03-17 01:17:10 +00:00
|
|
|
entities.append(entity)
|
2018-10-11 17:40:23 +00:00
|
|
|
|
|
|
|
return entities
|
|
|
|
|
|
|
|
|
2019-03-17 01:42:42 +00:00
|
|
|
def get_outbound_entity(entity: BaseEntity, private_key):
|
2019-03-03 01:09:25 +00:00
|
|
|
"""Get the correct outbound entity for this protocol.
|
|
|
|
|
|
|
|
We might have to look at entity values to decide the correct outbound entity.
|
|
|
|
If we cannot find one, we should raise as conversion cannot be guaranteed to the given protocol.
|
|
|
|
|
|
|
|
Private key of author is needed to be passed for signing the outbound entity.
|
|
|
|
|
|
|
|
:arg entity: An entity instance which can be of a base or protocol entity class.
|
|
|
|
:arg private_key: Private key of sender in str format
|
|
|
|
:returns: Protocol specific entity class instance.
|
|
|
|
:raises ValueError: If conversion cannot be done.
|
|
|
|
"""
|
|
|
|
if getattr(entity, "outbound_doc", None):
|
|
|
|
# If the entity already has an outbound doc, just return the entity as is
|
|
|
|
return entity
|
|
|
|
outbound = None
|
|
|
|
cls = entity.__class__
|
2019-07-20 22:29:58 +00:00
|
|
|
if cls in [
|
|
|
|
ActivitypubAccept, ActivitypubFollow, ActivitypubProfile, ActivitypubPost, ActivitypubComment,
|
|
|
|
ActivitypubRetraction,
|
|
|
|
]:
|
2019-03-03 01:09:25 +00:00
|
|
|
# Already fine
|
|
|
|
outbound = entity
|
2019-03-17 01:42:42 +00:00
|
|
|
elif cls == Accept:
|
|
|
|
outbound = ActivitypubAccept.from_base(entity)
|
2019-03-03 01:09:25 +00:00
|
|
|
elif cls == Follow:
|
|
|
|
outbound = ActivitypubFollow.from_base(entity)
|
2019-05-12 17:17:40 +00:00
|
|
|
elif cls == Post:
|
|
|
|
outbound = ActivitypubPost.from_base(entity)
|
2019-03-03 01:09:25 +00:00
|
|
|
elif cls == Profile:
|
|
|
|
outbound = ActivitypubProfile.from_base(entity)
|
2019-07-20 22:29:58 +00:00
|
|
|
elif cls == Retraction:
|
|
|
|
outbound = ActivitypubRetraction.from_base(entity)
|
2019-06-29 22:09:47 +00:00
|
|
|
elif cls == Comment:
|
|
|
|
outbound = ActivitypubComment.from_base(entity)
|
2019-03-03 01:09:25 +00:00
|
|
|
if not outbound:
|
|
|
|
raise ValueError("Don't know how to convert this base entity to ActivityPub protocol entities.")
|
|
|
|
# TODO LDS signing
|
|
|
|
# if isinstance(outbound, DiasporaRelayableMixin) and not outbound.signature:
|
|
|
|
# # Sign by author if not signed yet. We don't want to overwrite any existing signature in the case
|
|
|
|
# # that this is being sent by the parent author
|
|
|
|
# outbound.sign(private_key)
|
|
|
|
# # If missing, also add same signature to `parent_author_signature`. This is required at the moment
|
|
|
|
# # in all situations but is apparently being removed.
|
|
|
|
# # TODO: remove this once Diaspora removes the extra signature
|
|
|
|
# outbound.parent_signature = outbound.signature
|
|
|
|
return outbound
|
|
|
|
|
|
|
|
|
2018-10-11 17:40:23 +00:00
|
|
|
def message_to_objects(
|
2019-03-14 20:06:39 +00:00
|
|
|
message: Dict, sender: str, sender_key_fetcher: Callable[[str], str] = None, user: UserType = None,
|
2018-10-11 17:40:23 +00:00
|
|
|
) -> List:
|
|
|
|
"""
|
|
|
|
Takes in a message extracted by a protocol and maps it to entities.
|
|
|
|
"""
|
|
|
|
# We only really expect one element here for ActivityPub.
|
2019-03-17 21:54:06 +00:00
|
|
|
return element_to_objects(message)
|
2018-10-11 17:40:23 +00:00
|
|
|
|
|
|
|
|
2019-05-12 19:28:12 +00:00
|
|
|
def transform_attribute(key: str, value: Union[str, Dict, int], transformed: Dict, cls, is_object: bool) -> None:
|
2018-10-11 17:40:23 +00:00
|
|
|
if value is None:
|
|
|
|
value = ""
|
|
|
|
if key == "id":
|
2019-07-20 22:29:58 +00:00
|
|
|
if is_object:
|
|
|
|
if cls == ActivitypubRetraction:
|
|
|
|
transformed["target_id"] = value
|
|
|
|
transformed["entity_type"] = "Object"
|
|
|
|
else:
|
|
|
|
transformed["id"] = value
|
|
|
|
elif cls == ActivitypubProfile:
|
2019-03-17 14:09:34 +00:00
|
|
|
transformed["id"] = value
|
2018-10-11 17:40:23 +00:00
|
|
|
else:
|
2019-03-17 14:09:34 +00:00
|
|
|
transformed["activity_id"] = value
|
2018-10-11 17:40:23 +00:00
|
|
|
elif key == "actor":
|
2019-03-17 14:09:34 +00:00
|
|
|
transformed["actor_id"] = value
|
2019-05-12 20:07:43 +00:00
|
|
|
elif key == "content":
|
|
|
|
transformed["raw_content"] = value
|
2019-03-17 14:09:34 +00:00
|
|
|
elif key == "inboxes" and isinstance(value, dict):
|
|
|
|
if "inboxes" not in transformed:
|
|
|
|
transformed["inboxes"] = {"private": None, "public": None}
|
2019-03-17 19:33:58 +00:00
|
|
|
if value.get('sharedInbox'):
|
|
|
|
transformed["endpoints"]["public"] = value.get("sharedInbox")
|
2018-10-28 21:31:33 +00:00
|
|
|
elif key == "icon":
|
|
|
|
# TODO maybe we should ditch these size constants and instead have a more flexible dict for images
|
|
|
|
# so based on protocol there would either be one url or many by size name
|
2019-03-14 21:03:24 +00:00
|
|
|
if isinstance(value, dict):
|
2019-03-17 14:09:34 +00:00
|
|
|
transformed["image_urls"] = {
|
2019-03-14 21:03:24 +00:00
|
|
|
"small": value['url'],
|
|
|
|
"medium": value['url'],
|
|
|
|
"large": value['url'],
|
2019-03-17 14:09:34 +00:00
|
|
|
}
|
2019-03-14 21:03:24 +00:00
|
|
|
else:
|
2019-03-17 14:09:34 +00:00
|
|
|
transformed["image_urls"] = {
|
2019-03-14 21:03:24 +00:00
|
|
|
"small": value,
|
|
|
|
"medium": value,
|
|
|
|
"large": value,
|
2019-03-17 14:09:34 +00:00
|
|
|
}
|
|
|
|
elif key == "inbox":
|
|
|
|
if "inboxes" not in transformed:
|
|
|
|
transformed["inboxes"] = {"private": None, "public": None}
|
|
|
|
transformed["inboxes"]["private"] = value
|
2019-03-17 19:33:58 +00:00
|
|
|
if not transformed["inboxes"]["public"]:
|
|
|
|
transformed["inboxes"]["public"] = value
|
2019-06-29 22:02:27 +00:00
|
|
|
elif key == "inReplyTo":
|
|
|
|
transformed["target_id"] = value
|
2018-10-28 21:31:33 +00:00
|
|
|
elif key == "name":
|
2019-03-17 14:09:34 +00:00
|
|
|
transformed["name"] = value
|
2018-10-11 17:40:23 +00:00
|
|
|
elif key == "object":
|
|
|
|
if isinstance(value, dict):
|
2019-06-28 21:55:23 +00:00
|
|
|
if cls == ActivitypubAccept:
|
2019-03-17 21:54:06 +00:00
|
|
|
transformed["target_id"] = value.get("id")
|
2019-06-28 21:55:23 +00:00
|
|
|
elif cls == ActivitypubFollow:
|
|
|
|
transformed["target_id"] = value.get("object")
|
2019-03-17 21:54:06 +00:00
|
|
|
else:
|
2019-05-12 19:28:12 +00:00
|
|
|
transform_attributes(value, cls, transformed, is_object=True)
|
2018-10-11 17:40:23 +00:00
|
|
|
else:
|
2019-03-17 14:09:34 +00:00
|
|
|
transformed["target_id"] = value
|
2018-10-28 21:31:33 +00:00
|
|
|
elif key == "preferredUsername":
|
2019-03-17 14:09:34 +00:00
|
|
|
transformed["username"] = value
|
2018-10-28 21:31:33 +00:00
|
|
|
elif key == "publicKey":
|
2019-03-17 14:09:34 +00:00
|
|
|
transformed["public_key"] = value.get('publicKeyPem', '')
|
2019-05-12 20:07:43 +00:00
|
|
|
elif key == "summary" and cls == ActivitypubProfile:
|
2019-03-17 14:09:34 +00:00
|
|
|
transformed["raw_content"] = value
|
2019-05-12 20:30:35 +00:00
|
|
|
elif key in ("to", "cc"):
|
|
|
|
if isinstance(value, list) and NAMESPACE_PUBLIC in value:
|
|
|
|
transformed["public"] = True
|
2019-06-28 21:55:23 +00:00
|
|
|
elif key == "type":
|
|
|
|
if value == "Undo":
|
|
|
|
transformed["following"] = False
|
2018-10-28 21:31:33 +00:00
|
|
|
elif key == "url":
|
2019-03-17 14:09:34 +00:00
|
|
|
transformed["url"] = value
|
2018-10-11 17:40:23 +00:00
|
|
|
|
|
|
|
|
2019-05-12 19:28:12 +00:00
|
|
|
def transform_attributes(payload: Dict, cls, transformed: Dict = None, is_object: bool = False) -> Dict:
|
2019-03-17 14:09:34 +00:00
|
|
|
if not transformed:
|
|
|
|
transformed = {}
|
2018-10-11 17:40:23 +00:00
|
|
|
for key, value in payload.items():
|
2019-05-12 19:28:12 +00:00
|
|
|
transform_attribute(key, value, transformed, cls, is_object)
|
2018-10-11 17:40:23 +00:00
|
|
|
return transformed
|