distributed-press-api-client 0.3.0 → 0.4.0rc3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6731553fbdfb80318a3ffedd78b9805594fc9817a8554516c8ac3c8982df3345
4
- data.tar.gz: 3e5fb1bf9ac4526acec284834505c19a0811bc5c51fccc177a712073794e4ab8
3
+ metadata.gz: ee95dc9deb3cb19084a66ee33ee4970906619c6489ce0bf29b0aa9f90ae99d93
4
+ data.tar.gz: abcb6a90f1e16ad51aac21f15b79067c141f4cdc45edfbddc69704dd235adac7
5
5
  SHA512:
6
- metadata.gz: 2dc6826aaa09cccc207933c3fd9c60de6e5fed15f2c2e7617090079ee74f666ba3ae95c3a51d7543279dae1bc0be64564855ca883abe251b7df6fc1eb01d9f65
7
- data.tar.gz: 31835fadab543760c181269ab9284e0c52c91784f0c46f7f9e1ca7389926e0108bea25bf053ff2c3657acb5724ddc51bfff0249e74b2f3b97f21b2a4537fc72e
6
+ metadata.gz: 5d3e98400b29ee2258a29beb1c47fab70a07d1cbdd43a96df9842a507d2ee3f3aaf9d3eeef1dee381967363227fb8af68b0b9f080861feac67595b1a081c43b0
7
+ data.tar.gz: 55291cec6e026520a3e9f111fd80e55b4762aa551f1dcae12015951a01d9d6dd55bbda07394f5f36566e3500abed7673e2a71c90feaf2f4092ef838361d3c8e8
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-schema'
4
+
5
+ class DistributedPress
6
+ module V1
7
+ module Schemas
8
+ # Bittorrent protocol
9
+ class BittorrentProtocol < Dry::Schema::JSON
10
+ define do
11
+ required(:enabled).value(:bool)
12
+ # TODO: Validate URL
13
+ required(:link).filled(:string)
14
+ required(:gateway).filled(:string)
15
+ required(:dnslink).filled(:string)
16
+ required(:infoHash).filled(:string)
17
+ required(:pubKey).filled(:string)
18
+ required(:magnet).filled(:string)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -11,6 +11,7 @@ class DistributedPress
11
11
  define do
12
12
  # TODO: Validate domain name
13
13
  required(:domain).filled(:string)
14
+ optional(:public).filled(:bool)
14
15
 
15
16
  optional(:protocols).hash do
16
17
  optional(:http).filled(:bool)
@@ -4,6 +4,7 @@ require 'dry-schema'
4
4
  require_relative 'http_protocol'
5
5
  require_relative 'hyper_protocol'
6
6
  require_relative 'ipfs_protocol'
7
+ require_relative 'bittorrent_protocol'
7
8
 
8
9
  class DistributedPress
9
10
  module V1
@@ -15,18 +16,20 @@ class DistributedPress
15
16
 
16
17
  # TODO: Validate domain name
17
18
  required(:domain).filled(:string)
18
- required(:public).filled(:bool)
19
+ optional(:public).filled(:bool)
19
20
 
20
21
  required(:protocols).hash do
21
22
  required(:http).filled(:bool)
22
23
  required(:ipfs).filled(:bool)
23
24
  required(:hyper).filled(:bool)
25
+ optional(:bittorrent).filled(:bool)
24
26
  end
25
27
 
26
28
  required(:links).hash do
27
29
  optional(:http).hash(HttpProtocol.new)
28
30
  optional(:hyper).hash(HyperProtocol.new)
29
31
  optional(:ipfs).hash(IpfsProtocol.new)
32
+ optional(:bittorrent).hash(BittorrentProtocol.new)
30
33
  end
31
34
  end
32
35
  end
@@ -10,7 +10,7 @@ class DistributedPress
10
10
  class UpdateSite < Dry::Schema::JSON
11
11
  define do
12
12
  required(:id).filled(:string)
13
- required(:public).filled(:bool)
13
+ optional(:public).filled(:bool)
14
14
 
15
15
  required(:protocols).hash do
16
16
  optional(:http).filled(:bool)
@@ -4,6 +4,7 @@ require_relative 'schemas/admin'
4
4
  require_relative 'schemas/http_protocol'
5
5
  require_relative 'schemas/hyper_protocol'
6
6
  require_relative 'schemas/ipfs_protocol'
7
+ require_relative 'schemas/bittorrent_protocol'
7
8
  require_relative 'schemas/new_admin'
8
9
  require_relative 'schemas/new_publisher'
9
10
  require_relative 'schemas/new_site'
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'blocklist'
4
+
5
+ class DistributedPress
6
+ module V1
7
+ module Social
8
+ # Manages Social Inbox allowlist
9
+ #
10
+ # @example
11
+ # DistributedPress::V1::Social::Allowlist.new(client: client)
12
+ # DistributedPress::V1::Social::Allowlist.new(client: client, actor: '@sutty@sutty.nl')
13
+ class Allowlist < Blocklist
14
+ ENDPOINT = '/allowlist'
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'client'
4
+
5
+ class DistributedPress
6
+ module V1
7
+ module Social
8
+ # Manages Social Inbox blocklist
9
+ #
10
+ # @example
11
+ # DistributedPress::V1::Social::Blocklist.new(client: client)
12
+ # DistributedPress::V1::Social::Blocklist.new(client: client, actor: '@sutty@sutty.nl')
13
+ class Blocklist
14
+ ACCEPT = %w[text/plain].freeze
15
+ CONTENT_TYPE = 'text/plain'
16
+ ENDPOINT = '/blocklist'
17
+
18
+ # @return [DistributedPress::V1::Social::Client]
19
+ attr_reader :client
20
+
21
+ # @return [String,nil]
22
+ attr_reader :actor
23
+
24
+ # @param :client [DistributedPress::V1::Social::Client]
25
+ # @param :actor [String]
26
+ def initialize(client:, actor: nil)
27
+ @client = client
28
+ @actor = actor
29
+
30
+ @serializer = proc do |body|
31
+ body.join("\n")
32
+ end
33
+
34
+ # @param :body [String,nil]
35
+ # @return [Array<String>]
36
+ @parser =
37
+ proc do |body, _|
38
+ next [] if body.nil?
39
+
40
+ body.split("\n").map(&:strip).reject(&:empty?)
41
+ end
42
+ end
43
+
44
+ # Obtains the current blocklist
45
+ #
46
+ # @return [Array<String>]
47
+ def get
48
+ client.get(endpoint: endpoint, parser: @parser, content_type: CONTENT_TYPE, accept: ACCEPT)
49
+ end
50
+
51
+ # Sends an array of instances and accounts to be blocked
52
+ #
53
+ # @param :list [Array<String>]
54
+ # @return [HTTParty::Response]
55
+ def post(list:)
56
+ client.post(endpoint: endpoint, body: list, serializer: @serializer, parser: @parser,
57
+ content_type: CONTENT_TYPE, accept: ACCEPT)
58
+ end
59
+
60
+ # Removes instances and accounts from the blocklist
61
+ #
62
+ # @param :list [Array<String>]
63
+ # @return [HTTParty::Response]
64
+ def delete(list:)
65
+ client.delete(endpoint: endpoint, body: list, serializer: @serializer, parser: @parser,
66
+ content_type: CONTENT_TYPE, accept: ACCEPT)
67
+ end
68
+
69
+ def endpoint
70
+ @endpoint ||= "/v1/#{actor}#{self.class::ENDPOINT}".squeeze('/')
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'httparty'
4
+ require 'httparty/cache'
4
5
  require 'openssl'
5
6
  require 'time'
6
7
  require 'digest/sha2'
7
8
  require 'uri'
9
+ require_relative '../../version'
10
+ require_relative 'signed_headers'
8
11
 
9
12
  class DistributedPress
10
13
  module V1
@@ -17,22 +20,33 @@ class DistributedPress
17
20
  # @see {https://github.com/mastodon/mastodon/blob/main/app/lib/request.rb}
18
21
  class Client
19
22
  include ::HTTParty
23
+ include ::HTTParty::Cache
20
24
 
21
- # Signing algorithm
22
- #
23
- # @todo is it possible to use other algorithms?
24
- ALGORITHM = 'rsa-sha256'
25
- # Required by HTTP Signatures
26
- REQUEST_TARGET = '(request-target)'
27
- # Headers included in the signature
28
- SIGNABLE_HEADERS = [REQUEST_TARGET, 'Host', 'Date', 'Digest']
29
- # Content types
30
- CONTENT_TYPES = %w[application/ld+json application/activity+json]
25
+ class Error < StandardError; end
26
+ class ContentTooLargeError < Error; end
27
+ class BrotliUnsupportedError < Error; end
28
+
29
+ # Always cache
30
+ caching true
31
+
32
+ # Content types sent and accepted
33
+ ACCEPT = %w[application/activity+json application/ld+json application/json].freeze
34
+ CONTENT_TYPE = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
35
+
36
+ ACCEPT.each do |accept|
37
+ HTTParty::Parser::SupportedFormats[accept] = :json
38
+ end
31
39
 
32
40
  # API URL
41
+ #
33
42
  # @return [String]
34
43
  attr_reader :url
35
44
 
45
+ # API URI
46
+ #
47
+ # @return [URI]
48
+ attr_reader :uri
49
+
36
50
  # RSA key size
37
51
  #
38
52
  # @return [Integer]
@@ -43,36 +57,57 @@ class DistributedPress
43
57
  # @return [String]
44
58
  attr_reader :public_key_url
45
59
 
60
+ # Logger
61
+ #
62
+ # @return [Logger]
63
+ attr_reader :logger
64
+
46
65
  # @param :url [String] Social Distributed Press URL
47
66
  # @param :public_key [String] URL where public key is available
48
67
  # @param :private_key_pem [String] RSA Private key, PEM encoded
49
68
  # @param :key_size [Integer]
50
69
  # @param :logger [Logger]
51
- def initialize(url: 'https://social.distributed.press', public_key_url:, private_key_pem: nil, key_size: 2048, logger: nil)
52
- self.class.default_options[:base_uri] = @url = HTTParty.normalize_base_uri(url)
53
- self.class.default_options[:logger] = logger
70
+ # @param :cache_store [HTTParty::Cache::Store::Abstract,Symbol]
71
+ def initialize(public_key_url:, url: 'https://social.distributed.press', private_key_pem: nil, key_size: 2048,
72
+ logger: nil, cache_store: :memory)
73
+ self.class.default_options[:logger] = @logger = logger
54
74
  self.class.default_options[:log_level] = :debug
75
+ self.class.cache_store cache_store
55
76
 
77
+ @url = HTTParty.normalize_base_uri(url)
56
78
  @public_key_url = public_key_url
57
79
  @key_size = key_size
58
80
  @private_key_pem = private_key_pem
59
-
60
- CONTENT_TYPES.each do |content_type|
61
- HTTParty::Parser::SupportedFormats[content_type] = :json
81
+ @uri = URI.parse(url)
82
+ @serializer ||= proc do |body|
83
+ body.to_json
62
84
  end
63
85
  end
64
86
 
87
+ def _dump(_)
88
+ Marshal.dump({
89
+ public_key_url: public_key_url,
90
+ url: url,
91
+ private_key_pem: @private_key_pem,
92
+ key_size: key_size,
93
+ cache_store: self.class.cache_store
94
+ })
95
+ end
96
+
97
+ def self._load(hash)
98
+ new(**Marshal.load(hash))
99
+ end
100
+
65
101
  # GET request
66
102
  #
67
103
  # @param endpoint [String]
68
- # @return [Hash]
69
- def get(endpoint:)
70
- headers = default_headers
71
-
72
- add_request_specific_headers! headers, 'get', endpoint
73
- sign_headers! headers
74
-
75
- self.class.get(endpoint, headers: headers)
104
+ # @param parser [HTTParty::Parser,Proc]
105
+ # @param content_type [String]
106
+ # @param accept [Array<String>]
107
+ # @return [HTTParty::Response]
108
+ def get(endpoint:, parser: HTTParty::Parser, content_type: CONTENT_TYPE, accept: ACCEPT)
109
+ perform_signed_request method: Net::HTTP::Get, endpoint: endpoint, parser: parser, content_type: content_type,
110
+ accept: accept
76
111
  end
77
112
 
78
113
  # POST request
@@ -80,16 +115,45 @@ class DistributedPress
80
115
  # @todo Use DRY-schemas
81
116
  # @param endpoint [String]
82
117
  # @param body [Hash]
83
- # @return [Hash]
84
- def post(endpoint:, body:)
85
- body = body.to_json
86
- headers = default_headers
118
+ # @param serializer [Proc]
119
+ # @param parser [HTTParty::Parser,Proc]
120
+ # @param content_type [String]
121
+ # @param accept [Array<String>]
122
+ # @return [HTTParty::Response]
123
+ def post(endpoint:, body: nil, serializer: @serializer, parser: HTTParty::Parser, content_type: CONTENT_TYPE,
124
+ accept: ACCEPT)
125
+ perform_signed_request method: Net::HTTP::Post, endpoint: endpoint, body: body, serializer: serializer,
126
+ parser: parser, content_type: content_type, accept: accept
127
+ end
87
128
 
88
- add_request_specific_headers! headers, 'post', endpoint
89
- checksum_body! body, headers
90
- sign_headers! headers
129
+ # PUT request
130
+ #
131
+ # @param endpoint [String]
132
+ # @param body [Hash]
133
+ # @param serializer [Proc]
134
+ # @param parser [HTTParty::Parser,Proc]
135
+ # @param content_type [String]
136
+ # @param accept [Array<String>]
137
+ # @return [HTTParty::Response]
138
+ def put(endpoint:, body: nil, serializer: @serializer, parser: HTTParty::Parser, content_type: CONTENT_TYPE,
139
+ accept: ACCEPT)
140
+ perform_signed_request method: Net::HTTP::Put, endpoint: endpoint, body: body, serializer: serializer,
141
+ parser: parser, content_type: content_type, accept: accept
142
+ end
91
143
 
92
- self.class.post(endpoint, body: body, headers: headers)
144
+ # DELETE request with optional body
145
+ #
146
+ # @param endpoint [String]
147
+ # @param body [Hash]
148
+ # @param serializer [Proc]
149
+ # @param parser [HTTParty::Parser,Proc]
150
+ # @param content_type [String]
151
+ # @param accept [Array<String>]
152
+ # @return [HTTParty::Response]
153
+ def delete(endpoint:, body: nil, serializer: @serializer, parser: HTTParty::Parser, content_type: CONTENT_TYPE,
154
+ accept: ACCEPT)
155
+ perform_signed_request method: Net::HTTP::Delete, endpoint: endpoint, body: body, serializer: serializer,
156
+ parser: parser, content_type: content_type, accept: accept
93
157
  end
94
158
 
95
159
  # Loads or generates a private key
@@ -115,84 +179,154 @@ class DistributedPress
115
179
 
116
180
  private
117
181
 
118
- def default_headers
119
- {
182
+ # Perform request
183
+ #
184
+ # @param method [Net::HTTP::Get,Net::HTTP::Post,Net::HTTP::Put,Net::HTTP::Delete]
185
+ # @param endpoint [String]
186
+ # @param body [Hash]
187
+ # @param serializer [Proc]
188
+ # @param parser [HTTParty::Parser,Proc]
189
+ # @param content_type [String]
190
+ # @param accept [Array<String>]
191
+ # @return [HTTParty::Response]
192
+ def perform_signed_request(method:, endpoint:, body: nil, serializer: @default_serializer, parser: HTTParty::Parser,
193
+ content_type: CONTENT_TYPE, accept: ACCEPT)
194
+ headers = default_headers(content_type: content_type, accept: accept)
195
+
196
+ unless body.nil?
197
+ body = serializer.call(body)
198
+ checksum_body!(body, headers)
199
+ end
200
+
201
+ # Mimics HTTParty.perform_request, but processing the header
202
+ # after the request is instantiated. No need to process
203
+ # cookies for now. Uses the public key URL as a caching key so
204
+ # we revalidate access on shared caches.
205
+ options = { body: body, headers: headers, base_uri: url, parser: parser, stream_body: true, cache_key: public_key_url }
206
+ options = HTTParty::ModuleInheritableAttributes.hash_deep_dup(self.class.default_options).merge(options)
207
+ response_body = ''.dup
208
+
209
+ HTTParty::Request.new(method, endpoint, options).tap do |request|
210
+ headers.request = request
211
+ end.perform do |fragment|
212
+ fragment_to_body!(fragment, response_body)
213
+ end.tap do |response|
214
+ next if response_body.empty?
215
+
216
+ fix_response!(response, response_body)
217
+ end
218
+ end
219
+
220
+ # Default headers
221
+ #
222
+ # @param :content_type [String]
223
+ # @param :accept [Array<String>]
224
+ # @return [SignedHeaders]
225
+ def default_headers(content_type: CONTENT_TYPE, accept: ACCEPT)
226
+ SignedHeaders[
120
227
  'User-Agent' => "DistributedPress/#{DistributedPress::VERSION}",
121
- 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
122
- 'Host' => host
123
- }
228
+ 'Content-Type' => content_type,
229
+ 'Accept' => accept.join(', '),
230
+ 'Host' => host,
231
+ 'Accept-Encoding' => "#{'br;q=1.0,' if brotli?}gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
232
+ 'Date' => Time.now.utc.httpdate
233
+ ].tap do |headers|
234
+ headers.private_key = private_key
235
+ headers.public_key_url = public_key_url
236
+ end
124
237
  end
125
238
 
126
- # HTTP Signatures
239
+ # Generate a checksum for the request body
127
240
  #
128
- # @see {https://docs.joinmastodon.org/spec/security/}
241
+ # @param :body [String]
129
242
  # @param :headers [Hash]
130
- def sign_headers!(headers)
131
- headers['Signature'] = {
132
- 'keyId' => public_key_url,
133
- 'algorithm' => ALGORITHM,
134
- 'headers' => signable_headers(headers),
135
- 'signature' => signed_headers(headers)
136
- }.map do |key, value|
137
- "#{key}=\"#{value}\""
138
- end.join(',')
139
-
140
- headers.delete REQUEST_TARGET
243
+ def checksum_body!(body, headers)
244
+ headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest body}"
141
245
 
142
246
  nil
143
247
  end
144
248
 
145
- # List of headers to be signed, removing headers that don't
146
- # exist on the request.
249
+ # If the endpoint is on a subdirectory, we need the absolute
250
+ # path for signing
147
251
  #
252
+ # @param :endpoint [String]
148
253
  # @return [String]
149
- def signable_headers(headers)
150
- (SIGNABLE_HEADERS & headers.keys).join(' ').downcase
254
+ def absolute_endpoint(endpoint)
255
+ "#{uri.path}/#{endpoint}".squeeze('/')
151
256
  end
152
257
 
153
- # Sign headers
154
- #
155
- # @param :headers [Hash]
156
- # @return [String]
157
- def signed_headers(headers)
158
- Base64.strict_encode64(
159
- private_key.sign(
160
- OpenSSL::Digest.new('SHA256'),
161
- signature_content(headers)
162
- )
163
- )
258
+ # Brotli available
259
+ def brotli?
260
+ begin
261
+ require 'brs'
262
+ rescue LoadError => e
263
+ if logger
264
+ logger.warn e.message
265
+ else
266
+ puts e
267
+ end
268
+ end
269
+
270
+ defined? ::BRS
164
271
  end
165
272
 
166
- # Generates a string to be signed
273
+ # Inflates a Brotli compressed fragment. A fragment could be
274
+ # small enough to contain a huge inflated payload. In our
275
+ # tests, 2GB of zeroes were compressed to a 1.6KB fragment.
167
276
  #
168
- # @param :headers [Hash]
169
- # @return [String]
170
- def signature_content(headers)
171
- headers.slice(*SIGNABLE_HEADERS).map do |key, value|
172
- "#{key.downcase}: #{value}"
173
- end.join("\n")
277
+ # @todo Maybe each_char is too slow?
278
+ # @param fragment [HTTParty::ResponseFragment]
279
+ # @param append_to [String]
280
+ def inflate_fragment!(fragment, append_to)
281
+ fragment_io = StringIO.new(fragment)
282
+
283
+ BRS::Stream::Reader.new(fragment_io, source_buffer_length: 256,
284
+ destination_buffer_length: 1024).each_char do |char|
285
+ append_to << char
286
+
287
+ raise ContentTooLargeError if append_to.bytesize > 1_000_000
288
+ end
174
289
  end
175
290
 
176
- # Generate a checksum for the request body
291
+ # Collect fragments into body by checking the bytesize and
292
+ # failing if it gets too big.
177
293
  #
178
- # @param :body [String]
179
- # @param :headers [Hash]
180
- def checksum_body!(body, headers)
181
- headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest body}"
294
+ # @param :fragment [HTTParty::ResponseFragment]
295
+ # @param :append_to [String]
296
+ def fragment_to_body!(fragment, append_to)
297
+ case fragment.http_response['content-encoding']
298
+ when 'br'
299
+ # Raise an error rather than returning an empty body
300
+ unless brotli?
301
+ raise BrotliUnsupportedError,
302
+ 'Server sent brotli-encoded response but ruby-brs gem is missing'
303
+ end
182
304
 
183
- nil
305
+ inflate_fragment!(fragment, append_to)
306
+ else
307
+ append_to << fragment
308
+ end
309
+
310
+ raise ContentTooLargeError if append_to.bytesize > 1_000_000
311
+ rescue ContentTooLargeError
312
+ raise ContentTooLargeError, "Content too large #{append_to.bytesize} bytes!"
184
313
  end
185
314
 
186
- # Headers specific to a single request
315
+ # Re-adds a streamed body to the response and caches it again so
316
+ # it can carry the body
187
317
  #
188
- # @param :headers [Hash] Headers
189
- # @param :verb [String] HTTP verb
190
- # @param :endpoint [String] Path
191
- def add_request_specific_headers!(headers, verb, endpoint)
192
- headers['Date'] = Time.now.utc.httpdate
193
- headers[REQUEST_TARGET] = "#{verb} #{endpoint}"
318
+ # @param response [HTTParty::Response]
319
+ # @param body [String]
320
+ def fix_response!(response, body)
321
+ request = response.request
322
+ parser = request.options[:parser]
323
+ format = request.format || :plain
194
324
 
195
- nil
325
+ response.instance_variable_set(:@body, body)
326
+ response.instance_variable_set(:@parsed_block, -> { parser.call(body, format) })
327
+ response.instance_variable_set(:@parsed_response, nil)
328
+
329
+ self.class.cache_store.set(response.cache_key, response)
196
330
  end
197
331
  end
198
332
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'addressable'
4
+ require_relative 'reference'
5
+ require_relative 'referenced_object'
6
+
7
+ class DistributedPress
8
+ module V1
9
+ module Social
10
+ # Fetches ActivityStreams from different instances by
11
+ # instantiating clients.
12
+ class Dereferencer
13
+ # @return [DistributedPress::V1::Social::Client]
14
+ attr_reader :client
15
+
16
+ # We only need the client
17
+ def _dump(_)
18
+ Marshal.dump(client)
19
+ end
20
+
21
+ def self._load(client)
22
+ new(client: Marshal.load(client))
23
+ end
24
+
25
+ # @param :client [DistributedPress::V1::Social::Client]
26
+ def initialize(client:)
27
+ @client = client
28
+ @parser =
29
+ proc do |body, format|
30
+ next HTTParty::Parser.call(body, format || :plain) unless body&.starts_with? '{'
31
+
32
+ ReferencedObject.new(object: HTTParty::Parser.call(body, :json), dereferencer: self)
33
+ end
34
+ end
35
+
36
+ # Fetch a URI
37
+ #
38
+ # @param :uri [String, Addressable::URI]
39
+ # @return [HTTParty::Response]
40
+ def get(uri:)
41
+ uri = uris(uri)
42
+
43
+ clients(uri).get(endpoint: uri.path, parser: @parser)
44
+ end
45
+
46
+ # Gets a client for a URI
47
+ #
48
+ # @param :uri [Addressable::URI]
49
+ # @return [DistributedPress::V1::Social::Client]
50
+ def clients(uri)
51
+ @@clients ||= {}
52
+ @@clients[uri.origin] ||=
53
+ client.class.new(
54
+ url: uri.origin,
55
+ public_key_url: client.public_key_url,
56
+ private_key_pem: client.private_key.to_s,
57
+ logger: client.logger,
58
+ cache_store: client.class.cache_store
59
+ )
60
+ end
61
+
62
+ # Gets a reference for a URI and indexes it by the complete
63
+ # and normalized URI
64
+ #
65
+ # @param :uri [String, Addressable::URI]
66
+ # @return [DistributedPress::V1::Social::Reference]
67
+ def references(uri)
68
+ @@references ||= {}
69
+ @@references[uri.to_s] ||= Reference.new(uri: uri.to_s, dereferencer: self)
70
+ end
71
+
72
+ # Make sure we're getting a normalized Addressable::URI
73
+ #
74
+ # @param :uri [String, Addressable::URI]
75
+ # @return [Addressable::URI]
76
+ def uris(uri)
77
+ @@uris ||= {}
78
+ @@uris[uri.to_s] ||=
79
+ (if uri.is_a? Addressable::URI
80
+ uri
81
+ else
82
+ Addressable::URI.parse(uri)
83
+ end).normalize
84
+ end
85
+
86
+ end
87
+ end
88
+ end
89
+ end