diaspora_federation 0.0.13 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +15 -32
- data/lib/diaspora_federation.rb +9 -31
- data/lib/diaspora_federation/discovery/h_card.rb +8 -15
- data/lib/diaspora_federation/discovery/host_meta.rb +2 -4
- data/lib/diaspora_federation/discovery/web_finger.rb +11 -11
- data/lib/diaspora_federation/discovery/xrd_document.rb +4 -8
- data/lib/diaspora_federation/entities.rb +2 -0
- data/lib/diaspora_federation/entities/account_deletion.rb +5 -0
- data/lib/diaspora_federation/entities/comment.rb +3 -6
- data/lib/diaspora_federation/entities/contact.rb +5 -0
- data/lib/diaspora_federation/entities/conversation.rb +10 -1
- data/lib/diaspora_federation/entities/message.rb +29 -4
- data/lib/diaspora_federation/entities/participation.rb +24 -0
- data/lib/diaspora_federation/entities/poll_participation.rb +3 -6
- data/lib/diaspora_federation/entities/profile.rb +5 -0
- data/lib/diaspora_federation/entities/related_entity.rb +33 -0
- data/lib/diaspora_federation/entities/relayable.rb +55 -40
- data/lib/diaspora_federation/entities/relayable_retraction.rb +21 -12
- data/lib/diaspora_federation/entities/request.rb +6 -2
- data/lib/diaspora_federation/entities/reshare.rb +5 -0
- data/lib/diaspora_federation/entities/retraction.rb +37 -0
- data/lib/diaspora_federation/entities/signed_retraction.rb +16 -5
- data/lib/diaspora_federation/entities/status_message.rb +11 -0
- data/lib/diaspora_federation/entity.rb +73 -30
- data/lib/diaspora_federation/federation/fetcher.rb +11 -1
- data/lib/diaspora_federation/federation/receiver.rb +10 -0
- data/lib/diaspora_federation/federation/receiver/abstract_receiver.rb +18 -4
- data/lib/diaspora_federation/federation/receiver/exceptions.rb +4 -0
- data/lib/diaspora_federation/federation/receiver/public.rb +10 -0
- data/lib/diaspora_federation/federation/sender.rb +1 -1
- data/lib/diaspora_federation/http_client.rb +1 -2
- data/lib/diaspora_federation/logging.rb +6 -0
- data/lib/diaspora_federation/properties_dsl.rb +4 -2
- data/lib/diaspora_federation/salmon/encrypted_magic_envelope.rb +2 -2
- data/lib/diaspora_federation/salmon/encrypted_slap.rb +3 -5
- data/lib/diaspora_federation/salmon/exceptions.rb +1 -1
- data/lib/diaspora_federation/salmon/magic_envelope.rb +16 -17
- data/lib/diaspora_federation/salmon/slap.rb +1 -2
- data/lib/diaspora_federation/salmon/xml_payload.rb +1 -2
- data/lib/diaspora_federation/validators.rb +2 -0
- data/lib/diaspora_federation/validators/conversation_validator.rb +2 -0
- data/lib/diaspora_federation/validators/message_validator.rb +2 -2
- data/lib/diaspora_federation/validators/participation_validator.rb +3 -2
- data/lib/diaspora_federation/validators/poll_validator.rb +1 -0
- data/lib/diaspora_federation/validators/related_entity_validator.rb +12 -0
- data/lib/diaspora_federation/validators/relayable_retraction_validator.rb +1 -1
- data/lib/diaspora_federation/validators/relayable_validator.rb +1 -0
- data/lib/diaspora_federation/validators/retraction_validator.rb +1 -1
- data/lib/diaspora_federation/validators/rules/diaspora_id.rb +8 -11
- data/lib/diaspora_federation/validators/rules/diaspora_id_count.rb +1 -1
- data/lib/diaspora_federation/validators/signed_retraction_validator.rb +1 -1
- data/lib/diaspora_federation/validators/status_message_validator.rb +2 -0
- data/lib/diaspora_federation/validators/web_finger_validator.rb +2 -2
- data/lib/diaspora_federation/version.rb +1 -1
- metadata +9 -7
@@ -26,10 +26,35 @@ module DiasporaFederation
|
|
26
26
|
# @return [String] conversation guid
|
27
27
|
property :conversation_guid
|
28
28
|
|
29
|
-
#
|
30
|
-
#
|
31
|
-
|
32
|
-
|
29
|
+
# It is only valid to receive a {Message} from the author itself,
|
30
|
+
# or from the author of the parent {Conversation} if the author signature is valid.
|
31
|
+
# @deprecated remove after {Message} doesn't include {Relayable} anymore
|
32
|
+
def sender_valid?(sender)
|
33
|
+
sender == author || (sender == parent_author && verify_author_signature)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# @deprecated remove after {Message} doesn't include {Relayable} anymore
|
39
|
+
def verify_author_signature
|
40
|
+
verify_signature(author, :author_signature)
|
41
|
+
true
|
42
|
+
end
|
43
|
+
|
44
|
+
# @deprecated remove after {Message} doesn't include {Relayable} anymore
|
45
|
+
def parent_author
|
46
|
+
parent = DiasporaFederation.callbacks.trigger(:fetch_related_entity, "Conversation", conversation_guid)
|
47
|
+
raise Federation::Fetcher::NotFetchable, "parent of #{self} not found" unless parent
|
48
|
+
parent.author
|
49
|
+
end
|
50
|
+
|
51
|
+
# Default implementation, don't verify signatures for a {Message}.
|
52
|
+
# @see Entity.populate_entity
|
53
|
+
# @deprecated remove after {Message} doesn't include {Relayable} anymore
|
54
|
+
# @param [Nokogiri::XML::Element] root_node xml nodes
|
55
|
+
# @return [Entity] instance
|
56
|
+
private_class_method def self.populate_entity(root_node)
|
57
|
+
new({parent_guid: nil, parent: nil}.merge(entity_data(root_node)))
|
33
58
|
end
|
34
59
|
end
|
35
60
|
end
|
@@ -15,6 +15,30 @@ module DiasporaFederation
|
|
15
15
|
# currently only "Post" is supported.
|
16
16
|
# @return [String] parent type
|
17
17
|
property :parent_type, xml_name: :target_type
|
18
|
+
|
19
|
+
# It is only valid to receive a {Participation} from the author itself.
|
20
|
+
# @deprecated remove after {Participation} doesn't include {Relayable} anymore
|
21
|
+
def sender_valid?(sender)
|
22
|
+
sender == author
|
23
|
+
end
|
24
|
+
|
25
|
+
# validates that the parent exists and the parent author is local
|
26
|
+
def validate_parent
|
27
|
+
parent = DiasporaFederation.callbacks.trigger(:fetch_related_entity, parent_type, parent_guid)
|
28
|
+
raise ParentNotLocal, "obj=#{self}" unless parent && parent.local
|
29
|
+
end
|
30
|
+
|
31
|
+
# Don't verify signatures for a {Participation}. Validate that the parent is local.
|
32
|
+
# @see Entity.populate_entity
|
33
|
+
# @param [Nokogiri::XML::Element] root_node xml nodes
|
34
|
+
# @return [Entity] instance
|
35
|
+
private_class_method def self.populate_entity(root_node)
|
36
|
+
new(entity_data(root_node).merge(parent: nil)).tap(&:validate_parent)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Raised, if the parent is not owned by the receiving pod.
|
40
|
+
class ParentNotLocal < RuntimeError
|
41
|
+
end
|
18
42
|
end
|
19
43
|
end
|
20
44
|
end
|
@@ -8,6 +8,9 @@ module DiasporaFederation
|
|
8
8
|
# @deprecated
|
9
9
|
LEGACY_SIGNATURE_ORDER = %i(guid parent_guid author poll_answer_guid).freeze
|
10
10
|
|
11
|
+
# The {PollParticipation} parent is a {Poll}
|
12
|
+
PARENT_TYPE = "Poll".freeze
|
13
|
+
|
11
14
|
include Relayable
|
12
15
|
|
13
16
|
# @!attribute [r] poll_answer_guid
|
@@ -15,12 +18,6 @@ module DiasporaFederation
|
|
15
18
|
# @see PollAnswer#guid
|
16
19
|
# @return [String] poll answer guid
|
17
20
|
property :poll_answer_guid
|
18
|
-
|
19
|
-
# The {PollParticipation} parent is a {Poll}
|
20
|
-
# @return [String] parent type
|
21
|
-
def parent_type
|
22
|
-
"Poll"
|
23
|
-
end
|
24
21
|
end
|
25
22
|
end
|
26
23
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module DiasporaFederation
|
2
|
+
module Entities
|
3
|
+
# Entity meta informations for a related entity (parent or target of
|
4
|
+
# another entity).
|
5
|
+
class RelatedEntity < Entity
|
6
|
+
# @!attribute [r] author
|
7
|
+
# The diaspora ID of the author.
|
8
|
+
# @see Person#author
|
9
|
+
# @return [String] diaspora ID
|
10
|
+
property :author
|
11
|
+
|
12
|
+
# @!attribute [r] local
|
13
|
+
# +true+ if the owner of the entity is local on the pod
|
14
|
+
# @return [Boolean] is it a like or a dislike
|
15
|
+
property :local
|
16
|
+
|
17
|
+
# @!attribute [r] public
|
18
|
+
# shows whether the entity is visible to everyone or only to some aspects
|
19
|
+
# @return [Boolean] is it public
|
20
|
+
property :public, default: false
|
21
|
+
|
22
|
+
# @!attribute [r] parent
|
23
|
+
# if the entity also have a parent (Comment or Like), +nil+ if it has no parent
|
24
|
+
# @return [RelatedEntity] parent entity
|
25
|
+
entity :parent, Entities::RelatedEntity, default: nil
|
26
|
+
|
27
|
+
# never add {RelatedEntity} to xml
|
28
|
+
def to_xml
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -45,20 +45,24 @@ module DiasporaFederation
|
|
45
45
|
# This signature is required only when federation from upstream (parent) post author to
|
46
46
|
# downstream subscribers. This is the case when the parent author has to resend a relayable
|
47
47
|
# received from one of his subscribers to all others.
|
48
|
-
#
|
49
48
|
# @return [String] parent author signature
|
50
49
|
#
|
51
|
-
#
|
52
|
-
|
53
|
-
|
50
|
+
# @!attribute [r] parent
|
51
|
+
# meta information about the parent object
|
52
|
+
# @return [RelatedEntity] parent entity
|
53
|
+
#
|
54
|
+
# @param [Entity] klass the entity in which it is included
|
55
|
+
def self.included(klass)
|
56
|
+
klass.class_eval do
|
54
57
|
property :author, xml_name: :diaspora_handle
|
55
58
|
property :guid
|
56
59
|
property :parent_guid
|
57
60
|
property :author_signature, default: nil
|
58
61
|
property :parent_author_signature, default: nil
|
62
|
+
entity :parent, Entities::RelatedEntity
|
59
63
|
end
|
60
64
|
|
61
|
-
|
65
|
+
klass.extend ParseXML
|
62
66
|
end
|
63
67
|
|
64
68
|
# Initializes a new relayable Entity with order and additional xml elements
|
@@ -77,62 +81,59 @@ module DiasporaFederation
|
|
77
81
|
# verifies the signatures (+author_signature+ and +parent_author_signature+ if needed)
|
78
82
|
# @raise [SignatureVerificationFailed] if the signature is not valid or no public key is found
|
79
83
|
def verify_signatures
|
80
|
-
|
81
|
-
raise PublicKeyNotFound, "author_signature author=#{author} guid=#{guid}" if pubkey.nil?
|
82
|
-
raise SignatureVerificationFailed, "wrong author_signature" unless verify_signature(pubkey, author_signature)
|
84
|
+
verify_signature(author, :author_signature)
|
83
85
|
|
84
|
-
|
85
|
-
|
86
|
+
# this happens only on downstream federation
|
87
|
+
verify_signature(parent.author, :parent_author_signature) unless parent.local
|
86
88
|
end
|
87
89
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
def verify_parent_author_signature
|
92
|
-
pubkey = DiasporaFederation.callbacks.trigger(:fetch_author_public_key_by_entity_guid, parent_type, parent_guid)
|
90
|
+
def sender_valid?(sender)
|
91
|
+
sender == author || sender == parent.author
|
92
|
+
end
|
93
93
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
end
|
94
|
+
# @return [String] string representation of this object
|
95
|
+
def to_s
|
96
|
+
"#{super}#{":#{parent_type}" if respond_to?(:parent_type)}:#{parent_guid}"
|
98
97
|
end
|
99
98
|
|
99
|
+
private
|
100
|
+
|
100
101
|
# Check that signature is a correct signature
|
101
102
|
#
|
102
|
-
# @param [
|
103
|
-
# @param [String]
|
103
|
+
# @param [String] author The author of the signature
|
104
|
+
# @param [String] signature_key The signature to be verified
|
104
105
|
# @return [Boolean] signature valid
|
105
|
-
def verify_signature(
|
106
|
-
|
107
|
-
|
108
|
-
return false
|
109
|
-
end
|
106
|
+
def verify_signature(author, signature_key)
|
107
|
+
pubkey = DiasporaFederation.callbacks.trigger(:fetch_public_key, author)
|
108
|
+
raise PublicKeyNotFound, "signature=#{signature_key} person=#{author} obj=#{self}" if pubkey.nil?
|
110
109
|
|
111
|
-
|
112
|
-
|
113
|
-
|
110
|
+
signature = public_send(signature_key)
|
111
|
+
raise SignatureVerificationFailed, "no #{signature_key} for #{self}" if signature.nil?
|
112
|
+
|
113
|
+
valid = pubkey.verify(DIGEST, Base64.decode64(signature), signature_data)
|
114
|
+
raise SignatureVerificationFailed, "wrong #{signature_key} for #{self}" unless valid
|
115
|
+
|
116
|
+
logger.info "event=verify_signature signature=#{signature_key} status=valid obj=#{self}"
|
114
117
|
end
|
115
118
|
|
116
119
|
# sign with author key
|
117
120
|
# @raise [AuthorPrivateKeyNotFound] if the author private key is not found
|
118
121
|
# @return [String] A Base64 encoded signature of #signature_data with key
|
119
122
|
def sign_with_author
|
120
|
-
privkey = DiasporaFederation.callbacks.trigger(:
|
121
|
-
raise AuthorPrivateKeyNotFound, "author=#{author}
|
123
|
+
privkey = DiasporaFederation.callbacks.trigger(:fetch_private_key, author)
|
124
|
+
raise AuthorPrivateKeyNotFound, "author=#{author} obj=#{self}" if privkey.nil?
|
122
125
|
sign_with_key(privkey).tap do
|
123
|
-
logger.info "event=sign status=complete signature=author_signature author=#{author}
|
126
|
+
logger.info "event=sign status=complete signature=author_signature author=#{author} obj=#{self}"
|
124
127
|
end
|
125
128
|
end
|
126
129
|
|
127
130
|
# sign with parent author key, if the parent author is local (if the private key is found)
|
128
131
|
# @return [String] A Base64 encoded signature of #signature_data with key
|
129
132
|
def sign_with_parent_author_if_available
|
130
|
-
privkey = DiasporaFederation.callbacks.trigger(
|
131
|
-
:fetch_author_private_key_by_entity_guid, parent_type, parent_guid
|
132
|
-
)
|
133
|
+
privkey = DiasporaFederation.callbacks.trigger(:fetch_private_key, parent.author)
|
133
134
|
if privkey
|
134
135
|
sign_with_key(privkey).tap do
|
135
|
-
logger.info "event=sign status=complete signature=parent_author_signature
|
136
|
+
logger.info "event=sign status=complete signature=parent_author_signature obj=#{self}"
|
136
137
|
end
|
137
138
|
end
|
138
139
|
end
|
@@ -152,7 +153,7 @@ module DiasporaFederation
|
|
152
153
|
# @return [Hash] sorted xml elements with updated signatures
|
153
154
|
def xml_elements
|
154
155
|
xml_data = super.merge(additional_xml_elements)
|
155
|
-
|
156
|
+
signature_order.map {|element| [element, xml_data[element]] }.to_h.tap do |xml_elements|
|
156
157
|
xml_elements[:author_signature] = author_signature || sign_with_author
|
157
158
|
xml_elements[:parent_author_signature] = parent_author_signature || sign_with_parent_author_if_available.to_s
|
158
159
|
end
|
@@ -197,19 +198,33 @@ module DiasporaFederation
|
|
197
198
|
end
|
198
199
|
end
|
199
200
|
|
201
|
+
fetch_parent(entity_data)
|
200
202
|
new(entity_data, xml_order, additional_xml_elements).tap(&:verify_signatures)
|
201
203
|
end
|
204
|
+
|
205
|
+
def fetch_parent(data)
|
206
|
+
type = data[:parent_type] || self::PARENT_TYPE
|
207
|
+
guid = data[:parent_guid]
|
208
|
+
|
209
|
+
data[:parent] = DiasporaFederation.callbacks.trigger(:fetch_related_entity, type, guid)
|
210
|
+
|
211
|
+
unless data[:parent]
|
212
|
+
# fetch and receive parent from remote, if not available locally
|
213
|
+
Federation::Fetcher.fetch_public(data[:author], type, guid)
|
214
|
+
data[:parent] = DiasporaFederation.callbacks.trigger(:fetch_related_entity, type, guid)
|
215
|
+
end
|
216
|
+
end
|
202
217
|
end
|
203
218
|
|
204
|
-
#
|
219
|
+
# Raised, if creating the author_signature failes, because the private key was not found
|
205
220
|
class AuthorPrivateKeyNotFound < RuntimeError
|
206
221
|
end
|
207
222
|
|
208
|
-
#
|
223
|
+
# Raised, if verify_signatures fails to verify signatures (no public key found)
|
209
224
|
class PublicKeyNotFound < RuntimeError
|
210
225
|
end
|
211
226
|
|
212
|
-
#
|
227
|
+
# Raised, if verify_signatures fails to verify signatures (signatures are wrong)
|
213
228
|
class SignatureVerificationFailed < RuntimeError
|
214
229
|
end
|
215
230
|
end
|
@@ -53,42 +53,51 @@ module DiasporaFederation
|
|
53
53
|
# @return [String] target author signature
|
54
54
|
property :target_author_signature, default: nil
|
55
55
|
|
56
|
+
# @!attribute [r] target
|
57
|
+
# target entity
|
58
|
+
# @return [RelatedEntity] target entity
|
59
|
+
entity :target, Entities::RelatedEntity
|
60
|
+
|
56
61
|
# use only {Retraction} for receive
|
57
62
|
# @return [Retraction] instance as normal retraction
|
58
63
|
def to_retraction
|
59
|
-
Retraction.new(author: author, target_guid: target_guid, target_type: target_type)
|
64
|
+
Retraction.new(author: author, target_guid: target_guid, target_type: target_type, target: target)
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [String] string representation of this object
|
68
|
+
def to_s
|
69
|
+
"RelayableRetraction:#{target_type}:#{target_guid}"
|
60
70
|
end
|
61
71
|
|
62
72
|
private
|
63
73
|
|
64
74
|
# @param [Nokogiri::XML::Element] root_node xml nodes
|
65
75
|
# @return [Retraction] instance
|
66
|
-
def self.populate_entity(root_node)
|
67
|
-
|
76
|
+
private_class_method def self.populate_entity(root_node)
|
77
|
+
entity_data = entity_data(root_node)
|
78
|
+
entity_data[:target] = Retraction.send(:fetch_target, entity_data[:target_type], entity_data[:target_guid])
|
79
|
+
new(entity_data).to_retraction
|
68
80
|
end
|
69
|
-
private_class_method :populate_entity
|
70
81
|
|
71
82
|
# It updates also the signatures with the keys of the author and the parent
|
72
83
|
# if the signatures are not there yet and if the keys are available.
|
73
84
|
#
|
74
85
|
# @return [Hash] xml elements with updated signatures
|
75
86
|
def xml_elements
|
76
|
-
|
77
|
-
privkey = DiasporaFederation.callbacks.trigger(:fetch_private_key_by_diaspora_id, author)
|
87
|
+
privkey = DiasporaFederation.callbacks.trigger(:fetch_private_key, author)
|
78
88
|
|
79
89
|
super.tap do |xml_elements|
|
80
|
-
fill_required_signature(
|
90
|
+
fill_required_signature(privkey, xml_elements) unless privkey.nil?
|
81
91
|
end
|
82
92
|
end
|
83
93
|
|
84
|
-
# @param [String] target_author the author of the entity to retract
|
85
94
|
# @param [OpenSSL::PKey::RSA] privkey private key of sender
|
86
95
|
# @param [Hash] hash hash given for a signing
|
87
|
-
def fill_required_signature(
|
88
|
-
if
|
89
|
-
hash[:target_author_signature] = SignedRetraction.sign_with_key(privkey, self)
|
90
|
-
elsif target_author != author && parent_author_signature.nil?
|
96
|
+
def fill_required_signature(privkey, hash)
|
97
|
+
if target.parent.author == author && parent_author_signature.nil?
|
91
98
|
hash[:parent_author_signature] = SignedRetraction.sign_with_key(privkey, self)
|
99
|
+
elsif target.author == author && target_author_signature.nil?
|
100
|
+
hash[:target_author_signature] = SignedRetraction.sign_with_key(privkey, self)
|
92
101
|
end
|
93
102
|
end
|
94
103
|
end
|
@@ -24,12 +24,16 @@ module DiasporaFederation
|
|
24
24
|
Contact.new(author: author, recipient: recipient)
|
25
25
|
end
|
26
26
|
|
27
|
+
# @return [String] string representation of this object
|
28
|
+
def to_s
|
29
|
+
"Request:#{author}:#{recipient}"
|
30
|
+
end
|
31
|
+
|
27
32
|
# @param [Nokogiri::XML::Element] root_node xml nodes
|
28
33
|
# @return [Retraction] instance
|
29
|
-
def self.populate_entity(root_node)
|
34
|
+
private_class_method def self.populate_entity(root_node)
|
30
35
|
super(root_node).to_contact
|
31
36
|
end
|
32
|
-
private_class_method :populate_entity
|
33
37
|
end
|
34
38
|
end
|
35
39
|
end
|
@@ -22,6 +22,11 @@ module DiasporaFederation
|
|
22
22
|
# has no meaning at the moment
|
23
23
|
# @return [Boolean] public
|
24
24
|
property :public, default: true # always true? (we only reshare public posts)
|
25
|
+
|
26
|
+
# @return [String] string representation of this object
|
27
|
+
def to_s
|
28
|
+
"#{super}:#{root_guid}"
|
29
|
+
end
|
25
30
|
end
|
26
31
|
end
|
27
32
|
end
|
@@ -19,6 +19,43 @@ module DiasporaFederation
|
|
19
19
|
# A string describing the type of the target.
|
20
20
|
# @return [String] target type
|
21
21
|
property :target_type, xml_name: :type
|
22
|
+
|
23
|
+
# @!attribute [r] target
|
24
|
+
# target entity
|
25
|
+
# @return [RelatedEntity] target entity
|
26
|
+
entity :target, Entities::RelatedEntity
|
27
|
+
|
28
|
+
def sender_valid?(sender)
|
29
|
+
case target_type
|
30
|
+
when "Comment", "Like", "PollParticipation"
|
31
|
+
sender == target.author || sender == target.parent.author
|
32
|
+
else
|
33
|
+
sender == target.author
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [String] string representation of this object
|
38
|
+
def to_s
|
39
|
+
"Retraction:#{target_type}:#{target_guid}"
|
40
|
+
end
|
41
|
+
|
42
|
+
# @param [Nokogiri::XML::Element] root_node xml nodes
|
43
|
+
# @return [Retraction] instance
|
44
|
+
private_class_method def self.populate_entity(root_node)
|
45
|
+
entity_data = entity_data(root_node)
|
46
|
+
entity_data[:target] = fetch_target(entity_data[:target_type], entity_data[:target_guid])
|
47
|
+
new(entity_data)
|
48
|
+
end
|
49
|
+
|
50
|
+
private_class_method def self.fetch_target(target_type, target_guid)
|
51
|
+
DiasporaFederation.callbacks.trigger(:fetch_related_entity, target_type, target_guid).tap do |target|
|
52
|
+
raise TargetNotFound, "not found: #{target_type}:#{target_guid}" unless target
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Raised, if the target of the {Retraction} was not found.
|
57
|
+
class TargetNotFound < RuntimeError
|
58
|
+
end
|
22
59
|
end
|
23
60
|
end
|
24
61
|
end
|