pep8speaks: Hello @jaywink! Thanks for submitting the PR.







 - In the file [`federation/tests/entities/diaspora/test_entities.py`](https://github.com/jaywink/federation/blob/296c903b949d95cfdaf3d5c49e79391163621575/federation/tests/entities/diaspora/test_entities.py), following are the PEP8 issues :

> [Line 106:13](https://github.com/jaywink/federation/blob/296c903b949d95cfdaf3d5c49e79391163621575/federation/tests/entities/diaspora/test_entities.py#L106): [E122](https://duckduckgo.com/?q=pep8%20E122) continuation line missing indentation or outdented
> [Line 107:13](https://github.com/jaywink/federation/blob/296c903b949d95cfdaf3d5c49e79391163621575/federation/tests/entities/diaspora/test_entities.py#L107): [E122](https://duckduckgo.com/?q=pep8%20E122) continuation line missing indentation or outdented
> [Line 108:9](https://github.com/jaywink/federation/blob/296c903b949d95cfdaf3d5c49e79391163621575/federation/tests/entities/diaspora/test_entities.py#L108): [E122](https://duckduckgo.com/?q=pep8%20E122) continuation line missing indentation or outdented

 - In the file [`federation/tests/entities/diaspora/test_mappers.py`](https://github.com/jaywink/federation/blob/296c903b949d95cfdaf3d5c49e79391163621575/federation/tests/entities/diaspora/test_mappers.py), following are the PEP8 issues :

> [Line 203:30](https://github.com/jaywink/federation/blob/296c903b949d95cfdaf3d5c49e79391163621575/federation/tests/entities/diaspora/test_mappers.py#L203): [E712](https://duckduckgo.com/?q=pep8%20E712) comparison to True should be 'if cond is True:' or 'if cond:'








codecov[bot]: # [Codecov](https://codecov.io/gh/jaywink/federation/pull/100?src=pr&el=h1) Report
> Merging [#100](https://codecov.io/gh/jaywink/federation/pull/100?src=pr&el=desc) into [master](https://codecov.io/gh/jaywink/federation/commit/ebacea83b0d70f8e63ffacdf113f05600acc6451?src=pr&el=desc) will **increase** coverage by `0.15%`.
> The diff coverage is `97.95%`.

[![Impacted file tree graph](https://codecov.io/gh/jaywink/federation/pull/100/graphs/tree.svg?height=150&width=650&token=wkvcOv0pZ6&src=pr)](https://codecov.io/gh/jaywink/federation/pull/100?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #100      +/-   ##
==========================================
+ Coverage   91.67%   91.83%   +0.15%     
==========================================
  Files          17       17              
  Lines        1093     1102       +9     
==========================================
+ Hits         1002     1012      +10     
+ Misses         91       90       -1
```


| [Impacted Files](https://codecov.io/gh/jaywink/federation/pull/100?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [federation/entities/diaspora/mappers.py](https://codecov.io/gh/jaywink/federation/pull/100?src=pr&el=tree#diff-ZmVkZXJhdGlvbi9lbnRpdGllcy9kaWFzcG9yYS9tYXBwZXJzLnB5) | `99.33% <100%> (ø)` | :arrow_up: |
| [federation/entities/diaspora/entities.py](https://codecov.io/gh/jaywink/federation/pull/100?src=pr&el=tree#diff-ZmVkZXJhdGlvbi9lbnRpdGllcy9kaWFzcG9yYS9lbnRpdGllcy5weQ==) | `97.89% <100%> (+0.11%)` | :arrow_up: |
| [federation/entities/base.py](https://codecov.io/gh/jaywink/federation/pull/100?src=pr&el=tree#diff-ZmVkZXJhdGlvbi9lbnRpdGllcy9iYXNlLnB5) | `96.44% <97.36%> (+0.59%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/jaywink/federation/pull/100?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/jaywink/federation/pull/100?src=pr&el=footer). Last update [ebacea8...296c903](https://codecov.io/gh/jaywink/federation/pull/100?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 07c5f19..19755b4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
 
 ## [unreleased]
 
+### Added
+* Added base entity `Share` which maps to a `DiasporaReshare` for the Diaspora protocol. ([related issue](https://github.com/jaywink/federation/issues/94))
+
+  The `Share` entity supports all the properties that a Diaspora reshare does. Additionally two other properties are supported: `raw_content` and `entity_type`. The former can be used for a "quoted share" case where the sharer adds their own note to the share. The latter can be used to reference the type of object that was shared, to help the receiver, if it is not sharing a `Post` entity. The value must be a base entity class name.
+
 ### Fixed
 * Converting base entity `Profile` to `DiasporaProfile` for outbound sending missed two attributes, `image_urls` and `tag_list`. Those are now included so that the values transfer into the built payload.
 
diff --git a/docs/protocols.rst b/docs/protocols.rst
index 443cff1..a12f202 100644
--- a/docs/protocols.rst
+++ b/docs/protocols.rst
@@ -24,6 +24,7 @@ The feature set supported by this release is approximately the following:
    * Retraction
    * StatusMessage
    * Contact
+   * Reshare
 
 Implementation unfortunately currently requires knowledge of how Diaspora discovery works as the implementer has to implement all the necessary views correctly (even though this library provides document generators). However, the magic envelope, signature and entity building is all abstracted inside the library.
 
diff --git a/docs/usage.rst b/docs/usage.rst
index e69a918..5a84ebd 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -17,6 +17,7 @@ Entity types are as follows below.
 .. autoclass:: federation.entities.base.Reaction
 .. autoclass:: federation.entities.base.Relationship
 .. autoclass:: federation.entities.base.Retraction
+.. autoclass:: federation.entities.base.Share
 
 Protocol entities
 .................
diff --git a/federation/entities/base.py b/federation/entities/base.py
index 989516a..b8f8ba5 100644
--- a/federation/entities/base.py
+++ b/federation/entities/base.py
@@ -5,7 +5,7 @@ from dirty_validators.basic import Email
 
 
 __all__ = (
-    "Post", "Image", "Comment", "Reaction", "Relationship", "Profile", "Retraction", "Follow",
+    "Post", "Image", "Comment", "Reaction", "Relationship", "Profile", "Retraction", "Follow", "Share,"
 )
 
 
@@ -125,6 +125,24 @@ class TargetGUIDMixin(BaseEntity):
             raise ValueError("Target GUID must be at least 16 characters")
 
 
+class ParticipationMixin(TargetGUIDMixin):
+    """Reflects a participation to something."""
+    participation = ""
+
+    _participation_valid_values = ["reaction", "subscription", "comment"]
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self._required += ["participation"]
+
+    def validate_participation(self):
+        """Ensure participation is of a certain type."""
+        if self.participation not in self._participation_valid_values:
+            raise ValueError("participation should be one of: {valid}".format(
+                valid=", ".join(self._participation_valid_values)
+            ))
+
+
 class HandleMixin(BaseEntity):
     handle = ""
 
@@ -178,7 +196,43 @@ class OptionalRawContentMixin(RawContentMixin):
         self._required.remove("raw_content")
 
 
-class Image(GUIDMixin, HandleMixin, PublicMixin, OptionalRawContentMixin, CreatedAtMixin, BaseEntity):
+class EntityTypeMixin(BaseEntity):
+    """Provides a field for entity type.
+
+    Validates it is one of our entities.
+    """
+    entity_type = ""
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self._required += ["entity_type"]
+
+    def validate_entity_type(self):
+        """Ensure type is some entity we know of."""
+        if self.entity_type not in __all__:
+            raise ValueError("Entity type %s not recognized." % self.entity_type)
+
+
+class ProviderDisplayNameMixin(BaseEntity):
+    """Provides a field for provider display name."""
+    provider_display_name = ""
+
+
+class TargetHandleMixin(BaseEntity):
+    """Provides a target handle field."""
+    target_handle = ""
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self._required += ["target_handle"]
+
+    def validate_target_handle(self):
+        validator = Email()
+        if not validator.is_valid(self.target_handle):
+            raise ValueError("Target handle is not valid")
+
+
+class Image(GUIDMixin, HandleMixin, PublicMixin, OptionalRawContentMixin, CreatedAtMixin):
     """Reflects a single image, possibly linked to another object."""
     remote_path = ""
     remote_name = ""
@@ -192,32 +246,13 @@ class Image(GUIDMixin, HandleMixin, PublicMixin, OptionalRawContentMixin, Create
         self._required += ["remote_path", "remote_name"]
 
 
-class Post(RawContentMixin, GUIDMixin, HandleMixin, PublicMixin, CreatedAtMixin, BaseEntity):
+class Post(RawContentMixin, GUIDMixin, HandleMixin, PublicMixin, CreatedAtMixin, ProviderDisplayNameMixin):
     """Reflects a post, status message, etc, which will be composed from the message or to the message."""
-    provider_display_name = ""
     location = ""
 
     _allowed_children = (Image,)
 
 
-class ParticipationMixin(TargetGUIDMixin):
-    """Reflects a participation to something."""
-    participation = ""
-
-    _participation_valid_values = ["reaction", "subscription", "comment"]
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self._required += ["participation"]
-
-    def validate_participation(self):
-        """Ensure participation is of a certain type."""
-        if self.participation not in self._participation_valid_values:
-            raise ValueError("participation should be one of: {valid}".format(
-                valid=", ".join(self._participation_valid_values)
-            ))
-
-
 class Comment(RawContentMixin, GUIDMixin, ParticipationMixin, CreatedAtMixin, HandleMixin):
     """Represents a comment, linked to another object."""
     participation = "comment"
@@ -247,21 +282,15 @@ class Reaction(GUIDMixin, ParticipationMixin, CreatedAtMixin, HandleMixin):
             ))
 
 
-class Relationship(CreatedAtMixin, HandleMixin):
+class Relationship(CreatedAtMixin, HandleMixin, TargetHandleMixin):
     """Represents a relationship between two handles."""
-    target_handle = ""
     relationship = ""
 
     _relationship_valid_values = ["sharing", "following", "ignoring", "blocking"]
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
-        self._required += ["relationship", "target_handle"]
-
-    def validate_target_handle(self):
-        validator = Email()
-        if not validator.is_valid(self.target_handle):
-            raise ValueError("Target handle is not valid")
+        self._required += ["relationship"]
 
     def validate_relationship(self):
         """Ensure relationship is of a certain type."""
@@ -271,19 +300,13 @@ class Relationship(CreatedAtMixin, HandleMixin):
             ))
 
 
-class Follow(CreatedAtMixin, HandleMixin):
+class Follow(CreatedAtMixin, HandleMixin, TargetHandleMixin):
     """Represents a handle following or unfollowing another handle."""
-    target_handle = ""
     following = True
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
-        self._required += ["target_handle", "following"]
-
-    def validate_target_handle(self):
-        validator = Email()
-        if not validator.is_valid(self.target_handle):
-            raise ValueError("Target handle is not valid")
+        self._required += ["following"]
 
 
 class Profile(CreatedAtMixin, HandleMixin, OptionalRawContentMixin, PublicMixin, GUIDMixin):
@@ -313,15 +336,19 @@ class Profile(CreatedAtMixin, HandleMixin, OptionalRawContentMixin, PublicMixin,
                 raise ValueError("Email is not valid")
 
 
-class Retraction(CreatedAtMixin, HandleMixin, TargetGUIDMixin):
+class Retraction(CreatedAtMixin, HandleMixin, TargetGUIDMixin, EntityTypeMixin):
     """Represents a retraction of content by author."""
-    entity_type = ""
+    pass
 
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self._required += ["entity_type"]
 
-    def validate_entity_type(self):
-        """Ensure type is some entity we know of."""
-        if self.entity_type not in __all__:
-            raise ValueError("Entity type %s not recognized." % self.entity_type)
+class Share(CreatedAtMixin, HandleMixin, TargetGUIDMixin, GUIDMixin, EntityTypeMixin, OptionalRawContentMixin,
+            PublicMixin, ProviderDisplayNameMixin, TargetHandleMixin):
+    """Represents a share of another entity.
+
+    ``entity_type`` defaults to "Post" but can be any base entity class name. It should be the class name of the
+    entity that was shared.
+
+    The optional ``raw_content`` can be used for a "quoted share" case where the sharer adds their own note to the
+    share.
+    """
+    entity_type = "Post"
diff --git a/federation/entities/diaspora/entities.py b/federation/entities/diaspora/entities.py
index 5e23523..d7392aa 100644
--- a/federation/entities/diaspora/entities.py
+++ b/federation/entities/diaspora/entities.py
@@ -1,6 +1,7 @@
 from lxml import etree
 
-from federation.entities.base import Comment, Post, Reaction, Relationship, Profile, Retraction, BaseEntity, Follow
+from federation.entities.base import (
+    Comment, Post, Reaction, Relationship, Profile, Retraction, BaseEntity, Follow, Share)
 from federation.entities.diaspora.utils import format_dt, struct_to_xml, get_base_attributes, add_element_to_doc
 from federation.exceptions import SignatureVerificationError
 from federation.protocols.diaspora.signatures import verify_relayable_signature, create_relayable_signature
@@ -211,3 +212,22 @@ class DiasporaRetraction(DiasporaEntityMixin, Retraction):
             index = values.index(value)
             return list(DiasporaRetraction.mapped.keys())[index]
         return value
+
+
+class DiasporaReshare(DiasporaEntityMixin, Share):
+    """Diaspora Reshare."""
+    def to_xml(self):
+        element = etree.Element("reshare")
+        struct_to_xml(element, [
+            {"author": self.handle},
+            {"guid": self.guid},
+            {"created_at": format_dt(self.created_at)},
+            {"root_author": self.target_handle},
+            {"root_guid": self.target_guid},
+            {"provider_display_name": self.provider_display_name},
+            {"public": "true" if self.public else "false"},
+            # Some of our own not in Diaspora protocol
+            {"raw_content": self.raw_content},
+            {"entity_type": self.entity_type},
+        ])
+        return element
diff --git a/federation/entities/diaspora/mappers.py b/federation/entities/diaspora/mappers.py
index bb8a174..5acd205 100644
--- a/federation/entities/diaspora/mappers.py
+++ b/federation/entities/diaspora/mappers.py
@@ -3,10 +3,10 @@ from datetime import datetime
 
 from lxml import etree
 
-from federation.entities.base import Image, Relationship, Post, Reaction, Comment, Profile, Retraction, Follow
+from federation.entities.base import Image, Relationship, Post, Reaction, Comment, Profile, Retraction, Follow, Share
 from federation.entities.diaspora.entities import (
     DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest, DiasporaProfile, DiasporaRetraction,
-    DiasporaRelayableMixin, DiasporaContact)
+    DiasporaRelayableMixin, DiasporaContact, DiasporaReshare)
 from federation.protocols.diaspora.signatures import get_element_child_info
 from federation.utils.diaspora import retrieve_and_parse_profile
 
@@ -21,11 +21,12 @@ MAPPINGS = {
     "profile": DiasporaProfile,
     "retraction": DiasporaRetraction,
     "contact": DiasporaContact,
+    "reshare": DiasporaReshare,
 }
 
 TAGS = [
     # Order is important. Any top level tags should be before possibly child tags
-    "status_message", "comment", "like", "request", "profile", "retraction", "photo", "contact",
+    "reshare", "status_message", "comment", "like", "request", "profile", "retraction", "photo", "contact",
 ]
 
 BOOLEAN_KEYS = (
@@ -44,6 +45,7 @@ INTEGER_KEYS = (
     "width",
 )
 
+
 def xml_children_as_dict(node):
     """Turn the children of node <xml> into a dict, keyed by tag name.
 
@@ -167,9 +169,9 @@ def transform_attributes(attrs, cls):
             transformed["raw_content"] = value
         elif key in ["diaspora_handle", "sender_handle", "author"]:
             transformed["handle"] = value
-        elif key in ["recipient_handle", "recipient"]:
+        elif key in ["recipient_handle", "recipient", "root_author", "root_diaspora_id"]:
             transformed["target_handle"] = value
-        elif key == "parent_guid":
+        elif key in ["parent_guid", "post_guid", "root_guid"]:
             transformed["target_guid"] = value
         elif key == "first_name":
             transformed["name"] = value
@@ -203,8 +205,6 @@ def transform_attributes(attrs, cls):
             transformed["linked_type"] = "Post"
         elif key == "author_signature":
             transformed["signature"] = value
-        elif key == "post_guid":
-            transformed["target_guid"] = value
         elif key in BOOLEAN_KEYS:
             transformed[key] = True if value == "true" else False
         elif key in DATETIME_KEYS:
@@ -239,7 +239,7 @@ def get_outbound_entity(entity, private_key):
     outbound = None
     cls = entity.__class__
     if cls in [DiasporaPost, DiasporaRequest, DiasporaComment, DiasporaLike, DiasporaProfile, DiasporaRetraction,
-               DiasporaContact]:
+               DiasporaContact, DiasporaReshare]:
         # Already fine
         outbound = entity
     elif cls == Post:
@@ -259,6 +259,8 @@ def get_outbound_entity(entity, private_key):
         outbound = DiasporaProfile.from_base(entity)
     elif cls == Retraction:
         outbound = DiasporaRetraction.from_base(entity)
+    elif cls == Share:
+        outbound = DiasporaReshare.from_base(entity)
     if not outbound:
         raise ValueError("Don't know how to convert this base entity to Diaspora protocol entities.")
     if isinstance(outbound, DiasporaRelayableMixin) and not outbound.signature:

Add Diaspora Reshare entity support
https://diaspora.github.io/diaspora_federation/entities/reshare.html
diff --git a/federation/tests/entities/diaspora/test_entities.py b/federation/tests/entities/diaspora/test_entities.py
index 2f8671d..dea34c1 100644
--- a/federation/tests/entities/diaspora/test_entities.py
+++ b/federation/tests/entities/diaspora/test_entities.py
@@ -6,9 +6,10 @@ from lxml import etree
 from federation.entities.base import Profile
 from federation.entities.diaspora.entities import (
     DiasporaComment, DiasporaPost, DiasporaLike, DiasporaRequest, DiasporaProfile, DiasporaRetraction,
-    DiasporaContact)
+    DiasporaContact, DiasporaReshare)
 from federation.entities.diaspora.mappers import message_to_objects
 from federation.exceptions import SignatureVerificationError
+from federation.tests.factories.entities import ShareFactory
 from federation.tests.fixtures.keys import get_dummy_private_key
 from federation.tests.fixtures.payloads import DIASPORA_POST_COMMENT
 
@@ -93,6 +94,21 @@ class TestEntitiesConvertToXML:
                     b"<following>true</following><sharing>true</sharing></contact>"
         assert etree.tostring(result) == converted
 
+    def test_reshare_to_xml(self):
+        base_entity = ShareFactory()
+        entity = DiasporaReshare.from_base(base_entity)
+        result = entity.to_xml()
+        assert result.tag == "reshare"
+        result.find("created_at").text = ""  # timestamp makes testing painful
+        converted = "<reshare><author>%s</author><guid>%s</guid><created_at></created_at><root_author>%s" \
+                    "</root_author><root_guid>%s</root_guid><provider_display_name>%s</provider_display_name>" \
+                    "<public>%s</public><raw_content>%s</raw_content><entity_type>%s</entity_type></reshare>" % (
+                        entity.handle, entity.guid, entity.target_handle, entity.target_guid,
+                        entity.provider_display_name, "true" if entity.public else "false", entity.raw_content,
+                        entity.entity_type,
+                    )
+        assert etree.tostring(result).decode("utf-8") == converted
+
 
 class TestDiasporaProfileFillExtraAttributes:
     def test_raises_if_no_handle(self):
diff --git a/federation/tests/entities/diaspora/test_mappers.py b/federation/tests/entities/diaspora/test_mappers.py
index 9313596..1adeac0 100644
--- a/federation/tests/entities/diaspora/test_mappers.py
+++ b/federation/tests/entities/diaspora/test_mappers.py
@@ -6,17 +6,18 @@ import pytest
 
 from federation.entities.base import (
     Comment, Post, Reaction, Relationship, Profile, Retraction, Image,
-    Follow)
+    Follow, Share)
 from federation.entities.diaspora.entities import (
     DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest,
-    DiasporaProfile, DiasporaRetraction, DiasporaContact)
+    DiasporaProfile, DiasporaRetraction, DiasporaContact, DiasporaReshare)
 from federation.entities.diaspora.mappers import (
     message_to_objects, get_outbound_entity, check_sender_and_entity_handle_match)
 from federation.tests.fixtures.payloads import (
     DIASPORA_POST_SIMPLE, DIASPORA_POST_COMMENT, DIASPORA_POST_LIKE,
     DIASPORA_REQUEST, DIASPORA_PROFILE, DIASPORA_POST_INVALID, DIASPORA_RETRACTION,
     DIASPORA_POST_WITH_PHOTOS, DIASPORA_POST_LEGACY_TIMESTAMP, DIASPORA_POST_LEGACY, DIASPORA_CONTACT,
-    DIASPORA_LEGACY_REQUEST_RETRACTION, DIASPORA_POST_WITH_PHOTOS_2, DIASPORA_PROFILE_EMPTY_TAGS)
+    DIASPORA_LEGACY_REQUEST_RETRACTION, DIASPORA_POST_WITH_PHOTOS_2, DIASPORA_PROFILE_EMPTY_TAGS, DIASPORA_RESHARE,
+    DIASPORA_RESHARE_WITH_EXTRA_PROPERTIES, DIASPORA_RESHARE_LEGACY)
 
 
 def mock_fill(attributes):
@@ -189,6 +190,40 @@ class TestDiasporaEntityMappersReceive:
         assert entity.target_handle == "bob@example.org"
         assert entity.following is True
 
+    def test_message_to_objects_reshare(self):
+        entities = message_to_objects(DIASPORA_RESHARE, "alice@example.org")
+        assert len(entities) == 1
+        entity = entities[0]
+        assert isinstance(entity, DiasporaReshare)
+        assert entity.handle == "alice@example.org"
+        assert entity.guid == "a0b53e5029f6013487753131731751e9"
+        assert entity.provider_display_name == ""
+        assert entity.target_handle == "bob@example.com"
+        assert entity.target_guid == "a0b53bc029f6013487753131731751e9"
+        assert entity.public is True
+        assert entity.entity_type == "Post"
+
+    def test_message_to_objects_reshare_legacy(self):
+        entities = message_to_objects(DIASPORA_RESHARE_LEGACY, "alice@example.org")
+        assert len(entities) == 1
+        entity = entities[0]
+        assert isinstance(entity, DiasporaReshare)
+        assert entity.handle == "alice@example.org"
+        assert entity.guid == "a0b53e5029f6013487753131731751e9"
+        assert entity.provider_display_name == ""
+        assert entity.target_handle == "bob@example.com"
+        assert entity.target_guid == "a0b53bc029f6013487753131731751e9"
+        assert entity.public is True
+        assert entity.entity_type == "Post"
+
+    def test_message_to_objects_reshare_extra_properties(self):
+        entities = message_to_objects(DIASPORA_RESHARE_WITH_EXTRA_PROPERTIES, "alice@example.org")
+        assert len(entities) == 1
+        entity = entities[0]
+        assert isinstance(entity, DiasporaReshare)
+        assert entity.raw_content == "Important note here"
+        assert entity.entity_type == "Comment"
+
     @patch("federation.entities.diaspora.mappers.logger.error")
     def test_invalid_entity_logs_an_error(self, mock_logger):
         entities = message_to_objects(DIASPORA_POST_INVALID, "alice@alice.diaspora.example.org")
@@ -242,6 +277,8 @@ class TestGetOutboundEntity:
         assert get_outbound_entity(entity, private_key) == entity
         entity = DiasporaContact()
         assert get_outbound_entity(entity, private_key) == entity
+        entity = DiasporaReshare()
+        assert get_outbound_entity(entity, private_key) == entity
 
     def test_post_is_converted_to_diasporapost(self, private_key):
         entity = Post()
@@ -283,6 +320,10 @@ class TestGetOutboundEntity:
         entity = Follow()
         assert isinstance(get_outbound_entity(entity, private_key), DiasporaContact)
 
+    def test_share_is_converted_to_diasporareshare(self, private_key):
+        entity = Share()
+        assert isinstance(get_outbound_entity(entity, private_key), DiasporaReshare)
+
     def test_signs_relayable_if_no_signature(self, private_key):
         entity = DiasporaComment()
         outbound = get_outbound_entity(entity, private_key)
diff --git a/federation/tests/entities/test_base.py b/federation/tests/entities/test_base.py
index 5eaa10a..89985e3 100644
--- a/federation/tests/entities/test_base.py
+++ b/federation/tests/entities/test_base.py
@@ -4,11 +4,11 @@ import pytest
 
 from federation.entities.base import (
     BaseEntity, Relationship, Profile, RawContentMixin, GUIDMixin, HandleMixin, PublicMixin, Image, Retraction,
-    Follow)
-from federation.tests.factories.entities import TaggedPostFactory, PostFactory
+    Follow, TargetHandleMixin)
+from federation.tests.factories.entities import TaggedPostFactory, PostFactory, ShareFactory
 
 
-class TestPostEntityTags():
+class TestPostEntityTags:
     def test_post_entity_returns_list_of_tags(self):
         post = TaggedPostFactory()
         assert post.tags == {"tagone", "tagtwo", "tagthree", "upper", "snakecase"}
@@ -18,7 +18,7 @@ class TestPostEntityTags():
         assert post.tags == set()
 
 
-class TestBaseEntityCallsValidateMethods():
+class TestBaseEntityCallsValidateMethods:
     def test_entity_calls_attribute_validate_method(self):
         post = PostFactory()
         post.validate_location = Mock()
@@ -48,28 +48,41 @@ class TestBaseEntityCallsValidateMethods():
             post._validate_children()
 
 
-class TestGUIDMixinValidate():
+class TestGUIDMixinValidate:
     def test_validate_guid_raises_on_low_length(self):
         guid = GUIDMixin(guid="x"*15)
         with pytest.raises(ValueError):
             guid.validate()
+        guid = GUIDMixin(guid="x" * 16)
+        guid.validate()
 
 
-class TestHandleMixinValidate():
+class TestHandleMixinValidate:
     def test_validate_handle_raises_on_invalid_format(self):
         handle = HandleMixin(handle="foobar")
         with pytest.raises(ValueError):
             handle.validate()
+        handle = HandleMixin(handle="foobar@example.com")
+        handle.validate()
 
 
-class TestPublicMixinValidate():
+class TestTargetHandleMixinValidate:
+    def test_validate_target_handle_raises_on_invalid_format(self):
+        handle = TargetHandleMixin(target_handle="foobar")
+        with pytest.raises(ValueError):
+            handle.validate()
+        handle = TargetHandleMixin(target_handle="foobar@example.com")
+        handle.validate()
+
+
+class TestPublicMixinValidate:
     def test_validate_public_raises_on_low_length(self):
         public = PublicMixin(public="foobar")
         with pytest.raises(ValueError):
             public.validate()
 
 
-class TestEntityRequiredAttributes():
+class TestEntityRequiredAttributes:
     def test_entity_checks_for_required_attributes(self):
         entity = BaseEntity()
         entity._required = ["foobar"]
@@ -85,7 +98,7 @@ class TestEntityRequiredAttributes():
             entity.validate()
 
 
-class TestRelationshipEntity():
+class TestRelationshipEntity:
     def test_instance_creation(self):
         entity = Relationship(handle="bob@example.com", target_handle="alice@example.com", relationship="following")
         assert entity
@@ -95,13 +108,8 @@ class TestRelationshipEntity():
             entity = Relationship(handle="bob@example.com", target_handle="alice@example.com", relationship="hating")
             entity.validate()
 
-    def test_instance_creation_validates_target_handle_value(self):
-        with pytest.raises(ValueError):
-            entity = Relationship(handle="bob@example.com", target_handle="fefle.com", relationship="following")
-            entity.validate()
 
-
-class TestProfileEntity():
+class TestProfileEntity:
     def test_instance_creation(self):
         entity = Profile(handle="bob@example.com", raw_content="foobar")
         assert entity
@@ -117,7 +125,7 @@ class TestProfileEntity():
             entity.validate()
 
 
-class TestImageEntity():
+class TestImageEntity:
     def test_instance_creation(self):
         entity = Image(
             guid="x"*16, handle="foo@example.com", public=False, remote_path="foobar", remote_name="barfoo"
@@ -137,7 +145,7 @@ class TestImageEntity():
             entity.validate()
 
 
-class TestRetractionEntity():
+class TestRetractionEntity:
     def test_instance_creation(self):
         entity = Retraction(
             handle="foo@example.com", target_guid="x"*16, entity_type="Post"
@@ -162,16 +170,15 @@ class TestRetractionEntity():
             entity.validate()
 
 
-class TestFollowEntity():
+class TestFollowEntity:
     def test_instance_creation(self):
         entity = Follow(
             handle="foo@example.com", target_handle="bar@example.org", following=True
         )
         entity.validate()
 
-    def test_required_validates(self):
-        entity = Follow(
-            handle="foo@example.com", following=True
-        )
-        with pytest.raises(ValueError):
-            entity.validate()
+
+class TestShareEntity:
+    def test_instance_creation(self):
+        entity = ShareFactory()
+        entity.validate()
diff --git a/federation/tests/factories/entities.py b/federation/tests/factories/entities.py
index 7e9fdf0..3f4ab26 100644
--- a/federation/tests/factories/entities.py
+++ b/federation/tests/factories/entities.py
@@ -1,9 +1,8 @@
-# -*- coding: utf-8 -*-
 from random import shuffle
 import factory
 from factory import fuzzy
 
-from federation.entities.base import Post, Profile
+from federation.entities.base import Post, Profile, Share
 from federation.entities.diaspora.entities import DiasporaPost
 
 
@@ -47,3 +46,15 @@ class ProfileFactory(GUIDMixinFactory, HandleMixinFactory, RawContentMixinFactor
 
     name = fuzzy.FuzzyText(length=30)
     public_key = fuzzy.FuzzyText(length=300)
+
+
+class ShareFactory(GUIDMixinFactory, HandleMixinFactory):
+    class Meta:
+        model = Share
+
+    target_guid = factory.Faker("uuid4")
+    entity_type = "Post"
+    raw_content = ""
+    public = factory.Faker("pybool")
+    provider_display_name = ""
+    target_handle = factory.Faker("safe_email")
diff --git a/federation/tests/fixtures/payloads.py b/federation/tests/fixtures/payloads.py
index 973247b..5b5ae85 100644
--- a/federation/tests/fixtures/payloads.py
+++ b/federation/tests/fixtures/payloads.py
@@ -231,3 +231,41 @@ DIASPORA_CONTACT = """
         <sharing>true</sharing>
     </contact>
 """
+
+DIASPORA_RESHARE = """
+    <reshare>
+        <author>alice@example.org</author>
+        <guid>a0b53e5029f6013487753131731751e9</guid>
+        <created_at>2016-07-12T00:36:42Z</created_at>
+        <provider_display_name/>
+        <root_author>bob@example.com</root_author>
+        <root_guid>a0b53bc029f6013487753131731751e9</root_guid>
+        <public>true</public>
+    </reshare>
+"""
+
+DIASPORA_RESHARE_LEGACY = """
+    <reshare>
+        <diaspora_handle>alice@example.org</diaspora_handle>
+        <guid>a0b53e5029f6013487753131731751e9</guid>
+        <created_at>2016-07-12T00:36:42Z</created_at>
+        <provider_display_name/>
+        <root_diaspora_id>bob@example.com</root_diaspora_id>
+        <root_guid>a0b53bc029f6013487753131731751e9</root_guid>
+        <public>true</public>
+    </reshare>
+"""
+
+DIASPORA_RESHARE_WITH_EXTRA_PROPERTIES = """
+    <reshare>
+        <author>alice@example.org</author>
+        <guid>a0b53e5029f6013487753131731751e9</guid>
+        <created_at>2016-07-12T00:36:42Z</created_at>
+        <provider_display_name/>
+        <root_author>bob@example.com</root_author>
+        <root_guid>a0b53bc029f6013487753131731751e9</root_guid>
+        <public>true</public>
+        <raw_content>Important note here</raw_content>
+        <entity_type>Comment</entity_type>
+    </reshare>
+"""

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-warnings
apt-get update apt-get install -y gcc
3.6
dev-requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
arrow==1.2.3
attrs==22.2.0
Babel==2.11.0
certifi==2021.5.30
charset-normalizer==2.0.12
codecov==2.1.13
colorama==0.4.5
commonmark==0.9.1
coverage==6.2
cssselect==1.1.0
dirty-validators==0.5.4
docutils==0.18.1
factory-boy==3.2.1
Faker==14.2.1
-e git+https://github.com/jaywink/federation.git@ebacea83b0d70f8e63ffacdf113f05600acc6451#egg=federation
freezegun==1.2.2
idna==3.10
imagesize==1.4.1
importlib-metadata==4.8.3
iniconfig==1.1.1
isodate==0.6.1
Jinja2==3.0.3
jsonschema==3.2.0
livereload==2.6.3
lxml==5.3.1
MarkupSafe==2.0.1
packaging==21.3
pluggy==1.0.0
py==1.11.0
pycrypto==2.6.1
Pygments==2.14.0
pyparsing==3.1.4
pyrsistent==0.18.0
pytest==7.0.1
pytest-cov==4.0.0
pytest-warnings==0.3.1
python-dateutil==2.9.0.post0
python-xrd==0.1
pytz==2025.2
recommonmark==0.7.1
requests==2.27.1
six==1.17.0
snowballstemmer==2.2.0
Sphinx==5.3.0
sphinx-autobuild==2021.3.14
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
tomli==1.2.3
tornado==6.1
typing_extensions==4.1.1
urllib3==1.26.20
zipp==3.6.0

name: federation
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - arrow==1.2.3
      - attrs==22.2.0
      - babel==2.11.0
      - charset-normalizer==2.0.12
      - codecov==2.1.13
      - colorama==0.4.5
      - commonmark==0.9.1
      - coverage==6.2
      - cssselect==1.1.0
      - dirty-validators==0.5.4
      - docutils==0.18.1
      - factory-boy==3.2.1
      - faker==14.2.1
      - freezegun==1.2.2
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - isodate==0.6.1
      - jinja2==3.0.3
      - jsonschema==3.2.0
      - livereload==2.6.3
      - lxml==5.3.1
      - markupsafe==2.0.1
      - packaging==21.3
      - pluggy==1.0.0
      - py==1.11.0
      - pycrypto==2.6.1
      - pygments==2.14.0
      - pyparsing==3.1.4
      - pyrsistent==0.18.0
      - pytest==7.0.1
      - pytest-cov==4.0.0
      - pytest-warnings==0.3.1
      - python-dateutil==2.9.0.post0
      - python-xrd==0.1
      - pytz==2025.2
      - recommonmark==0.7.1
      - requests==2.27.1
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==5.3.0
      - sphinx-autobuild==2021.3.14
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - tomli==1.2.3
      - tornado==6.1
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - zipp==3.6.0
prefix: /opt/conda/envs/federation

federation/tests/entities/diaspora/test_entities.py::TestEntitiesConvertToXML::test_post_to_xml federation/tests/entities/diaspora/test_entities.py::TestEntitiesConvertToXML::test_comment_to_xml federation/tests/entities/diaspora/test_entities.py::TestEntitiesConvertToXML::test_like_to_xml federation/tests/entities/diaspora/test_entities.py::TestEntitiesConvertToXML::test_request_to_xml federation/tests/entities/diaspora/test_entities.py::TestEntitiesConvertToXML::test_profile_to_xml federation/tests/entities/diaspora/test_entities.py::TestEntitiesConvertToXML::test_retraction_to_xml federation/tests/entities/diaspora/test_entities.py::TestEntitiesConvertToXML::test_contact_to_xml federation/tests/entities/diaspora/test_entities.py::TestEntitiesConvertToXML::test_reshare_to_xml federation/tests/entities/diaspora/test_entities.py::TestDiasporaProfileFillExtraAttributes::test_raises_if_no_handle federation/tests/entities/diaspora/test_entities.py::TestDiasporaProfileFillExtraAttributes::test_calls_retrieve_and_parse_profile federation/tests/entities/diaspora/test_entities.py::TestDiasporaRetractionEntityConverters::test_entity_type_from_remote federation/tests/entities/diaspora/test_entities.py::TestDiasporaRetractionEntityConverters::test_entity_type_to_remote federation/tests/entities/diaspora/test_entities.py::TestDiasporaRelayableMixin::test_signing_comment_works federation/tests/entities/diaspora/test_entities.py::TestDiasporaRelayableMixin::test_signing_like_works federation/tests/entities/diaspora/test_entities.py::TestDiasporaRelayableMixin::test_sign_with_parent federation/tests/entities/diaspora/test_entities.py::TestDiasporaRelayableEntityValidate::test_raises_if_no_sender_key federation/tests/entities/diaspora/test_entities.py::TestDiasporaRelayableEntityValidate::test_calls_verify_signature federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_message_to_objects_simple_post federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_message_to_objects_post_legacy federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_message_to_objects_legact_timestamp federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_message_to_objects_post_with_photos federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_message_to_objects_comment federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_message_to_objects_like federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_message_to_objects_request federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_message_to_objects_profile federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_message_to_objects_profile_survives_empty_tag_string federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_message_to_objects_retraction federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_message_to_objects_retraction_legacy_request federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_message_to_objects_contact federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_message_to_objects_reshare federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_message_to_objects_reshare_legacy federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_message_to_objects_reshare_extra_properties federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_invalid_entity_logs_an_error federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_adds_source_protocol_to_entity federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_source_object federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_element_to_objects_calls_sender_key_fetcher federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_element_to_objects_calls_retrieve_remote_profile federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_element_to_objects_verifies_handles_are_the_same federation/tests/entities/diaspora/test_mappers.py::TestDiasporaEntityMappersReceive::test_element_to_objects_returns_no_entity_if_handles_are_different federation/tests/entities/diaspora/test_mappers.py::TestGetOutboundEntity::test_already_fine_entities_are_returned_as_is federation/tests/entities/diaspora/test_mappers.py::TestGetOutboundEntity::test_post_is_converted_to_diasporapost federation/tests/entities/diaspora/test_mappers.py::TestGetOutboundEntity::test_comment_is_converted_to_diasporacomment federation/tests/entities/diaspora/test_mappers.py::TestGetOutboundEntity::test_reaction_of_like_is_converted_to_diasporalike federation/tests/entities/diaspora/test_mappers.py::TestGetOutboundEntity::test_relationship_of_sharing_or_following_is_converted_to_diasporarequest federation/tests/entities/diaspora/test_mappers.py::TestGetOutboundEntity::test_profile_is_converted_to_diasporaprofile federation/tests/entities/diaspora/test_mappers.py::TestGetOutboundEntity::test_other_reaction_raises federation/tests/entities/diaspora/test_mappers.py::TestGetOutboundEntity::test_other_relation_raises federation/tests/entities/diaspora/test_mappers.py::TestGetOutboundEntity::test_retraction_is_converted_to_diasporaretraction federation/tests/entities/diaspora/test_mappers.py::TestGetOutboundEntity::test_follow_is_converted_to_diasporacontact federation/tests/entities/diaspora/test_mappers.py::TestGetOutboundEntity::test_share_is_converted_to_diasporareshare federation/tests/entities/diaspora/test_mappers.py::TestGetOutboundEntity::test_signs_relayable_if_no_signature federation/tests/entities/diaspora/test_mappers.py::TestGetOutboundEntity::test_returns_entity_if_outbound_doc_on_entity federation/tests/entities/diaspora/test_mappers.py::test_check_sender_and_entity_handle_match federation/tests/entities/test_base.py::TestPostEntityTags::test_post_entity_returns_list_of_tags federation/tests/entities/test_base.py::TestPostEntityTags::test_post_entity_without_raw_content_tags_returns_empty_set federation/tests/entities/test_base.py::TestBaseEntityCallsValidateMethods::test_entity_calls_attribute_validate_method federation/tests/entities/test_base.py::TestBaseEntityCallsValidateMethods::test_entity_calls_main_validate_methods federation/tests/entities/test_base.py::TestBaseEntityCallsValidateMethods::test_validate_children federation/tests/entities/test_base.py::TestGUIDMixinValidate::test_validate_guid_raises_on_low_length federation/tests/entities/test_base.py::TestHandleMixinValidate::test_validate_handle_raises_on_invalid_format federation/tests/entities/test_base.py::TestTargetHandleMixinValidate::test_validate_target_handle_raises_on_invalid_format federation/tests/entities/test_base.py::TestPublicMixinValidate::test_validate_public_raises_on_low_length federation/tests/entities/test_base.py::TestEntityRequiredAttributes::test_entity_checks_for_required_attributes federation/tests/entities/test_base.py::TestEntityRequiredAttributes::test_validate_checks_required_values_are_not_empty federation/tests/entities/test_base.py::TestRelationshipEntity::test_instance_creation federation/tests/entities/test_base.py::TestRelationshipEntity::test_instance_creation_validates_relationship_value federation/tests/entities/test_base.py::TestProfileEntity::test_instance_creation federation/tests/entities/test_base.py::TestProfileEntity::test_instance_creation_validates_email_value federation/tests/entities/test_base.py::TestProfileEntity::test_guid_is_mandatory federation/tests/entities/test_base.py::TestImageEntity::test_instance_creation federation/tests/entities/test_base.py::TestImageEntity::test_required_fields federation/tests/entities/test_base.py::TestRetractionEntity::test_instance_creation federation/tests/entities/test_base.py::TestRetractionEntity::test_required_validates federation/tests/entities/test_base.py::TestFollowEntity::test_instance_creation federation/tests/entities/test_base.py::TestShareEntity::test_instance_creation
federation/entities/diaspora/mappers.py CHANGELOG.md federation/entities/base.py docs/protocols.rst federation/entities/diaspora/entities.py docs/usage.rst
federation/entities/diaspora/mappers.py CHANGELOG.md federation/entities/base.py docs/protocols.rst federation/entities/diaspora/entities.py docs/usage.rst