coarnotify 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0c275102a17471d02d892d5fd97cef39a6de4df156789c1b39057deffb3f83b6
4
+ data.tar.gz: 7667b1a6ff67dab4a44bb3c0f1faac5f5baf9e4300603bc8c82883ba7a30ebb1
5
+ SHA512:
6
+ metadata.gz: 389e163e70f37372f625466b213a2c67cbd7298d3166d064ad7636298741c6976818b0d0a3a3a85149c7e50b08b30996280d904bd73053bab9fd13087006094b
7
+ data.tar.gz: '018e0f9c74b240ab33719bda796ebd3644121307cda75a62c713bacd5440fee8a8bdf4c2bd00fd23052eb5bf50714ffc1473f7c0367e74487fc10ab4e08310f8'
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative 'exceptions'
5
+ require_relative 'http'
6
+ require_relative 'core/notify'
7
+
8
+ module Coarnotify
9
+ # This module contains all the client-specific code for sending notifications
10
+ # to an inbox and receiving the responses it may return
11
+ module Client
12
+ # An object representing the response from a COAR Notify inbox.
13
+ #
14
+ # This contains the action that was carried out on the server:
15
+ #
16
+ # * CREATED - a new resource was created
17
+ # * ACCEPTED - the request was accepted, but the resource was not yet created
18
+ #
19
+ # In the event that the resource is created, then there will also be a location
20
+ # URL which will give you access to the resource
21
+ class NotifyResponse
22
+ CREATED = "created"
23
+ ACCEPTED = "accepted"
24
+
25
+ attr_reader :action, :location
26
+
27
+ # Construct a new NotifyResponse object with the action (created or accepted) and the location URL (optional)
28
+ #
29
+ # @param action [String] The action which the server said it took
30
+ # @param location [String, nil] The HTTP URI for the resource that was created (if present)
31
+ def initialize(action, location = nil)
32
+ @action = action
33
+ @location = location
34
+ end
35
+ end
36
+
37
+ # The COAR Notify Client, which is the mechanism through which you will interact with external inboxes.
38
+ #
39
+ # If you do not supply an inbox URL at construction you will
40
+ # need to supply it via the inbox_url= setter, or when you send a notification
41
+ class COARNotifyClient
42
+ attr_accessor :inbox_url
43
+
44
+ # Initialize the COAR Notify Client
45
+ #
46
+ # @param inbox_url [String, nil] HTTP URI of the inbox to communicate with by default
47
+ # @param http_layer [Http::HttpLayer, nil] An implementation of the HttpLayer interface to use for sending HTTP requests
48
+ def initialize(inbox_url: nil, http_layer: nil)
49
+ @inbox_url = inbox_url
50
+ @http = http_layer || Http::NetHttpLayer.new
51
+ end
52
+
53
+ # Send the given notification to the inbox. If no inbox URL is provided, the default inbox URL will be used.
54
+ #
55
+ # @param notification [Core::Notify::NotifyPattern] The notification object
56
+ # @param inbox_url [String, nil] The HTTP URI to send the notification to
57
+ # @param validate [Boolean] Whether to validate the notification before sending
58
+ # @return [NotifyResponse] a NotifyResponse object representing the response from the server
59
+ def send(notification, inbox_url: nil, validate: true)
60
+ inbox_url ||= @inbox_url
61
+ inbox_url ||= notification.target&.inbox
62
+
63
+ raise ArgumentError, "No inbox URL provided at the client, method, or notification level" if inbox_url.nil?
64
+
65
+ if validate
66
+ begin
67
+ notification.validate
68
+ rescue ValidationError => e
69
+ raise NotifyException, "Attempting to send invalid notification; to override set validate: false when calling this method"
70
+ end
71
+ end
72
+
73
+ resp = @http.post(inbox_url,
74
+ JSON.generate(notification.to_jsonld),
75
+ { "Content-Type" => "application/ld+json;profile=\"https://www.w3.org/ns/activitystreams\"" })
76
+
77
+ case resp.status_code
78
+ when 201
79
+ NotifyResponse.new(NotifyResponse::CREATED, resp.header("Location"))
80
+ when 202
81
+ NotifyResponse.new(NotifyResponse::ACCEPTED)
82
+ else
83
+ raise NotifyException, "Unexpected response: #{resp.status_code}"
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Coarnotify
6
+ module Core
7
+ # This module contains everything COAR Notify needs to know about ActivityStreams 2.0
8
+ # https://www.w3.org/TR/activitystreams-core/
9
+ #
10
+ # It provides knowledge of the essential AS properties and types, and a class to wrap
11
+ # ActivityStreams objects and provide a simple interface to work with them.
12
+ #
13
+ # **NOTE** this is not a complete implementation of AS 2.0, it is **only** what is required
14
+ # to work with COAR Notify patterns.
15
+ module ActivityStreams2
16
+ # Namespace for Activity Streams, to be used to construct namespaced properties used in COAR Notify Patterns
17
+ ACTIVITY_STREAMS_NAMESPACE = "https://www.w3.org/ns/activitystreams"
18
+
19
+ # ActivityStreams 2.0 properties used in COAR Notify Patterns
20
+ #
21
+ # These are provided as arrays, where the first element is the property name, and the second element is the namespace.
22
+ #
23
+ # These are suitable to be used as property names in all the property getters/setters in the notify pattern objects
24
+ # and in the validation configuration.
25
+ module Properties
26
+ # id property
27
+ ID = ["id", ACTIVITY_STREAMS_NAMESPACE].freeze
28
+
29
+ # type property
30
+ TYPE = ["type", ACTIVITY_STREAMS_NAMESPACE].freeze
31
+
32
+ # origin property
33
+ ORIGIN = ["origin", ACTIVITY_STREAMS_NAMESPACE].freeze
34
+
35
+ # object property
36
+ OBJECT = ["object", ACTIVITY_STREAMS_NAMESPACE].freeze
37
+
38
+ # target property
39
+ TARGET = ["target", ACTIVITY_STREAMS_NAMESPACE].freeze
40
+
41
+ # actor property
42
+ ACTOR = ["actor", ACTIVITY_STREAMS_NAMESPACE].freeze
43
+
44
+ # inReplyTo property
45
+ IN_REPLY_TO = ["inReplyTo", ACTIVITY_STREAMS_NAMESPACE].freeze
46
+
47
+ # context property
48
+ CONTEXT = ["context", ACTIVITY_STREAMS_NAMESPACE].freeze
49
+
50
+ # summary property
51
+ SUMMARY = ["summary", ACTIVITY_STREAMS_NAMESPACE].freeze
52
+
53
+ # as:subject property
54
+ SUBJECT_TRIPLE = ["as:subject", ACTIVITY_STREAMS_NAMESPACE].freeze
55
+
56
+ # as:object property
57
+ OBJECT_TRIPLE = ["as:object", ACTIVITY_STREAMS_NAMESPACE].freeze
58
+
59
+ # as:relationship property
60
+ RELATIONSHIP_TRIPLE = ["as:relationship", ACTIVITY_STREAMS_NAMESPACE].freeze
61
+ end
62
+
63
+ # List of all the Activity Streams types COAR Notify may use.
64
+ #
65
+ # Note that COAR Notify also has its own custom types and they are defined in
66
+ # Coarnotify::Core::Notify::NotifyTypes
67
+ module ActivityStreamsTypes
68
+ # Activities
69
+ ACCEPT = "Accept"
70
+ ANNOUNCE = "Announce"
71
+ REJECT = "Reject"
72
+ OFFER = "Offer"
73
+ TENTATIVE_ACCEPT = "TentativeAccept"
74
+ TENTATIVE_REJECT = "TentativeReject"
75
+ FLAG = "Flag"
76
+ UNDO = "Undo"
77
+
78
+ # Objects
79
+ ACTIVITY = "Activity"
80
+ APPLICATION = "Application"
81
+ ARTICLE = "Article"
82
+ AUDIO = "Audio"
83
+ COLLECTION = "Collection"
84
+ COLLECTION_PAGE = "CollectionPage"
85
+ RELATIONSHIP = "Relationship"
86
+ DOCUMENT = "Document"
87
+ EVENT = "Event"
88
+ GROUP = "Group"
89
+ IMAGE = "Image"
90
+ INTRANSITIVE_ACTIVITY = "IntransitiveActivity"
91
+ NOTE = "Note"
92
+ OBJECT = "Object"
93
+ ORDERED_COLLECTION = "OrderedCollection"
94
+ ORDERED_COLLECTION_PAGE = "OrderedCollectionPage"
95
+ ORGANIZATION = "Organization"
96
+ PAGE = "Page"
97
+ PERSON = "Person"
98
+ PLACE = "Place"
99
+ PROFILE = "Profile"
100
+ QUESTION = "Question"
101
+ SERVICE = "Service"
102
+ TOMBSTONE = "Tombstone"
103
+ VIDEO = "Video"
104
+ end
105
+
106
+ # The sub-list of ActivityStreams types that are also objects in AS 2.0
107
+ ACTIVITY_STREAMS_OBJECTS = [
108
+ ActivityStreamsTypes::ACTIVITY,
109
+ ActivityStreamsTypes::APPLICATION,
110
+ ActivityStreamsTypes::ARTICLE,
111
+ ActivityStreamsTypes::AUDIO,
112
+ ActivityStreamsTypes::COLLECTION,
113
+ ActivityStreamsTypes::COLLECTION_PAGE,
114
+ ActivityStreamsTypes::RELATIONSHIP,
115
+ ActivityStreamsTypes::DOCUMENT,
116
+ ActivityStreamsTypes::EVENT,
117
+ ActivityStreamsTypes::GROUP,
118
+ ActivityStreamsTypes::IMAGE,
119
+ ActivityStreamsTypes::INTRANSITIVE_ACTIVITY,
120
+ ActivityStreamsTypes::NOTE,
121
+ ActivityStreamsTypes::OBJECT,
122
+ ActivityStreamsTypes::ORDERED_COLLECTION,
123
+ ActivityStreamsTypes::ORDERED_COLLECTION_PAGE,
124
+ ActivityStreamsTypes::ORGANIZATION,
125
+ ActivityStreamsTypes::PAGE,
126
+ ActivityStreamsTypes::PERSON,
127
+ ActivityStreamsTypes::PLACE,
128
+ ActivityStreamsTypes::PROFILE,
129
+ ActivityStreamsTypes::QUESTION,
130
+ ActivityStreamsTypes::SERVICE,
131
+ ActivityStreamsTypes::TOMBSTONE,
132
+ ActivityStreamsTypes::VIDEO
133
+ ].freeze
134
+
135
+ # A simple wrapper around an ActivityStreams hash object
136
+ #
137
+ # Construct it with a ruby hash that represents an ActivityStreams object, or
138
+ # without to create a fresh, blank object.
139
+ class ActivityStream
140
+ attr_reader :doc, :context
141
+
142
+ # Construct a new ActivityStream object
143
+ #
144
+ # @param raw [Hash] the raw ActivityStreams object, as a hash
145
+ def initialize(raw = nil)
146
+ @doc = raw || {}
147
+ @context = []
148
+ if @doc.key?("@context")
149
+ @context = @doc["@context"]
150
+ @context = [@context] unless @context.is_a?(Array)
151
+ @doc.delete("@context")
152
+ end
153
+ end
154
+
155
+ # Set the document hash
156
+ #
157
+ # @param doc [Hash] the document hash to set
158
+ def doc=(doc)
159
+ @doc = doc
160
+ end
161
+
162
+ # Set the context
163
+ #
164
+ # @param context [Array, String] the context to set
165
+ def context=(context)
166
+ @context = context
167
+ end
168
+
169
+ # Register a namespace in the context of the ActivityStream
170
+ #
171
+ # @param namespace [String, Array] the namespace to register
172
+ def register_namespace(namespace)
173
+ entry = namespace
174
+ if namespace.is_a?(Array)
175
+ url = namespace[1]
176
+ short = namespace[0]
177
+ entry = { short => url }
178
+ end
179
+
180
+ @context << entry unless @context.include?(entry)
181
+ end
182
+
183
+ # Set an arbitrary property on the object. The property name can be one of:
184
+ #
185
+ # * A simple string with the property name
186
+ # * An array of the property name and the full namespace ["name", "http://example.com/ns"]
187
+ # * An array containing the property name and another array of the short name and the full namespace ["name", ["as", "http://example.com/ns"]]
188
+ #
189
+ # @param property [String, Array] the property name
190
+ # @param value [Object] the value to set
191
+ def set_property(property, value)
192
+ prop_name = property
193
+ namespace = nil
194
+ if property.is_a?(Array)
195
+ prop_name = property[0]
196
+ namespace = property[1]
197
+ end
198
+
199
+ @doc[prop_name] = value
200
+ register_namespace(namespace) if namespace
201
+ end
202
+
203
+ # Get an arbitrary property on the object. The property name can be one of:
204
+ #
205
+ # * A simple string with the property name
206
+ # * An array of the property name and the full namespace ["name", "http://example.com/ns"]
207
+ # * An array containing the property name and another array of the short name and the full namespace ["name", ["as", "http://example.com/ns"]]
208
+ #
209
+ # @param property [String, Array] the property name
210
+ # @return [Object] the value of the property, or nil if it does not exist
211
+ def get_property(property)
212
+ prop_name = property
213
+ namespace = nil
214
+ if property.is_a?(Array)
215
+ prop_name = property[0]
216
+ namespace = property[1]
217
+ end
218
+
219
+ @doc[prop_name]
220
+ end
221
+
222
+ # Get the activity stream as a JSON-LD object
223
+ #
224
+ # @return [Hash] the JSON-LD representation
225
+ def to_jsonld
226
+ {
227
+ "@context" => @context,
228
+ **@doc
229
+ }
230
+ end
231
+ end
232
+ end
233
+ end
234
+ end