distributed-press-api-client 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/distributed_press/v1/schemas/bittorrent_protocol.rb +1 -1
- data/lib/distributed_press/v1/social/allowlist.rb +18 -0
- data/lib/distributed_press/v1/social/blocklist.rb +75 -0
- data/lib/distributed_press/v1/social/client.rb +220 -86
- data/lib/distributed_press/v1/social/dereferencer.rb +89 -0
- data/lib/distributed_press/v1/social/hook.rb +100 -0
- data/lib/distributed_press/v1/social/inbox.rb +83 -0
- data/lib/distributed_press/v1/social/outbox.rb +50 -0
- data/lib/distributed_press/v1/social/reference.rb +39 -0
- data/lib/distributed_press/v1/social/referenced_object.rb +89 -0
- data/lib/distributed_press/v1/social/schemas/webhook.rb +24 -0
- data/lib/distributed_press/v1/social/signed_headers.rb +106 -0
- data/lib/distributed_press/version.rb +1 -1
- data/lib/dry/schema/processor_decorator.rb +25 -0
- data/lib/dry/schema/result_decorator.rb +35 -0
- metadata +42 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1a027ef2456449498ff03679402ac4a0f3689656cd5990e0aa9498b07e86beea
|
4
|
+
data.tar.gz: 1117e426318b07a0bbb833713b6e15fd5c614490d4ff05d1c29cac4cbbb8fc1a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c462c24bd87a73557218beefe37b3194016ee1462fbd061155018b09c068fba019027512c17eee71c8096f83bd10a14a81aaca06d6597efa9218e615341f24de
|
7
|
+
data.tar.gz: b9424cca5bf2d55085298e5205df63bfe6813b1a3874f3854c85a1cd9019a4207284468b5337081e2e73b7fbe7b9e8f6ceccc1e289d7a7a28fd856674ca5acb1
|
@@ -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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
#
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
61
|
-
|
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
|
-
# @
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
# @
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
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
|
-
|
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 }
|
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' =>
|
122
|
-
'
|
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
|
-
#
|
239
|
+
# Generate a checksum for the request body
|
127
240
|
#
|
128
|
-
# @
|
241
|
+
# @param :body [String]
|
129
242
|
# @param :headers [Hash]
|
130
|
-
def
|
131
|
-
headers['
|
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
|
-
#
|
146
|
-
#
|
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
|
150
|
-
|
254
|
+
def absolute_endpoint(endpoint)
|
255
|
+
"#{uri.path}/#{endpoint}".squeeze('/')
|
151
256
|
end
|
152
257
|
|
153
|
-
#
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
-
#
|
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
|
-
# @
|
169
|
-
# @
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
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
|
-
#
|
291
|
+
# Collect fragments into body by checking the bytesize and
|
292
|
+
# failing if it gets too big.
|
177
293
|
#
|
178
|
-
# @param :
|
179
|
-
# @param :
|
180
|
-
def
|
181
|
-
|
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
|
-
|
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
|
-
#
|
315
|
+
# Re-adds a streamed body to the response and caches it again so
|
316
|
+
# it can carry the body
|
187
317
|
#
|
188
|
-
# @param
|
189
|
-
# @param
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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
|
-
|
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
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'client'
|
4
|
+
require_relative 'schemas/webhook'
|
5
|
+
|
6
|
+
class DistributedPress
|
7
|
+
module V1
|
8
|
+
module Social
|
9
|
+
# Manages the actor's hooks on the Social Inbox
|
10
|
+
class Hook
|
11
|
+
class Error < StandardError; end
|
12
|
+
class ValidationError < Error; end
|
13
|
+
class EventNotValidError < Error; end
|
14
|
+
|
15
|
+
ACCEPT = %w[text/plain].freeze
|
16
|
+
CONTENT_TYPE = 'text/plain'
|
17
|
+
EVENTS = %w[moderationqueued onapproved onrejected].freeze
|
18
|
+
|
19
|
+
# @return [DistributedPress::V1::Social::Client]
|
20
|
+
attr_reader :client
|
21
|
+
|
22
|
+
# @return [String]
|
23
|
+
attr_reader :actor
|
24
|
+
|
25
|
+
# @param :client [DistributedPress::V1::Social::Client]
|
26
|
+
# @param :actor [String]
|
27
|
+
def initialize(client:, actor:)
|
28
|
+
@client = client
|
29
|
+
@actor = actor
|
30
|
+
# The serializer validates the body format and raises an
|
31
|
+
# exception if it contains errors.
|
32
|
+
@serializer = proc do |body|
|
33
|
+
body.tap do |b|
|
34
|
+
next if b.errors.empty?
|
35
|
+
|
36
|
+
raise ValidationError, body.errors.to_h.map do |key, messages|
|
37
|
+
messages.map do |message|
|
38
|
+
"#{key} #{message}"
|
39
|
+
end
|
40
|
+
end.flatten.join(', ')
|
41
|
+
end.to_h.to_json
|
42
|
+
end
|
43
|
+
|
44
|
+
# XXX: format is nil but should be :json
|
45
|
+
@parser = proc do |body, format|
|
46
|
+
next HTTParty::Parser.call(body, format || :plain) unless body.starts_with? '{'
|
47
|
+
|
48
|
+
json = HTTParty::Parser.call(body, :json)
|
49
|
+
|
50
|
+
DistributedPress::V1::Social::Schemas::Webhook.new.call(json)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Gets a hook and validates return, or 404 when not found
|
55
|
+
#
|
56
|
+
# @param :event [String]
|
57
|
+
# @return [HTTParty::Response]
|
58
|
+
def get(event:)
|
59
|
+
validate_event! event
|
60
|
+
|
61
|
+
client.get(endpoint: "#{endpoint}/#{event}", parser: @parser)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Creates a webhook
|
65
|
+
#
|
66
|
+
# @param :event [String]
|
67
|
+
# @param :hook [DistributedPress::V1::Social::Schemas::Webhook]
|
68
|
+
# @return [HTTParty::Response]
|
69
|
+
def put(event:, hook:)
|
70
|
+
validate_event! event
|
71
|
+
|
72
|
+
client.put(endpoint: "#{endpoint}/#{event}", body: hook, serializer: @serializer, parser: @parser)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Removes a hook for an event
|
76
|
+
#
|
77
|
+
# @param :event [String]
|
78
|
+
# @return [HTTParty::Response]
|
79
|
+
def delete(event:)
|
80
|
+
validate_event! event
|
81
|
+
|
82
|
+
client.delete(endpoint: "#{endpoint}/#{event}", serializer: @serializer, parser: @parser)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Endpoint
|
86
|
+
#
|
87
|
+
# @return [String]
|
88
|
+
def endpoint
|
89
|
+
@endpoint ||= "/v1/#{actor}/hooks"
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def validate_event!(event)
|
95
|
+
raise EventNotValidError, "#{event} must be one of #{EVENTS.join(', ')}" unless EVENTS.include? event
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require_relative 'client'
|
5
|
+
|
6
|
+
class DistributedPress
|
7
|
+
module V1
|
8
|
+
module Social
|
9
|
+
# Manages the actor's inbox on the Social Inbox
|
10
|
+
class Inbox
|
11
|
+
# @return [DistributedPress::V1::Social::Client]
|
12
|
+
attr_reader :client
|
13
|
+
|
14
|
+
# @return [String]
|
15
|
+
attr_reader :actor
|
16
|
+
|
17
|
+
# @param :client [DistributedPress::V1::Social::Client]
|
18
|
+
# @param :actor [String]
|
19
|
+
def initialize(client:, actor:)
|
20
|
+
@client = client
|
21
|
+
@actor = actor
|
22
|
+
end
|
23
|
+
|
24
|
+
# Creates a Social Inbox by uploading the keypair to it.
|
25
|
+
#
|
26
|
+
# @param actor_url [String] The URL where the Actor profile is hosted
|
27
|
+
# @return [HTTParty::Response]
|
28
|
+
def create(actor_url)
|
29
|
+
inbox_body = {
|
30
|
+
'actorUrl' => actor_url,
|
31
|
+
'publicKeyId' => "#{actor_url}#main-key",
|
32
|
+
'keypair' => {
|
33
|
+
'publicKeyPem' => client.public_key.public_to_pem,
|
34
|
+
'privateKeyPem' => client.private_key.export
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
client.post(endpoint: "/v1/#{actor}", body: inbox_body)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Get the actor's inbox
|
42
|
+
#
|
43
|
+
# @return [HTTParty::Response]
|
44
|
+
def get
|
45
|
+
client.get(endpoint: endpoint)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Send an activity to the actor's inbox. This is typically done
|
49
|
+
# by other actors though, not ourselves, so it could be used to
|
50
|
+
# send directly to another Actor's Social Inbox.
|
51
|
+
#
|
52
|
+
# @param :activity [Hash]
|
53
|
+
# @return [HTTParty::Response]
|
54
|
+
def post(activity:)
|
55
|
+
client.post(endpoint: endpoint, body: activity)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Reject an activity queued on the inbox
|
59
|
+
#
|
60
|
+
# @param :id [String] Activity ID
|
61
|
+
# @return [HTTParty::Response]
|
62
|
+
def reject(id:)
|
63
|
+
client.delete(endpoint: "#{endpoint}/#{URI.encode_uri_component(id)}")
|
64
|
+
end
|
65
|
+
|
66
|
+
# Accept an activity queued on the inbox
|
67
|
+
#
|
68
|
+
# @param :id [String] Activity ID
|
69
|
+
# @return [HTTParty::Response]
|
70
|
+
def accept(id:)
|
71
|
+
client.post(endpoint: "#{endpoint}/#{URI.encode_uri_component(id)}", body: {})
|
72
|
+
end
|
73
|
+
|
74
|
+
# Inbox
|
75
|
+
#
|
76
|
+
# @return [String]
|
77
|
+
def endpoint
|
78
|
+
@endpoint ||= "/v1/#{actor}/inbox"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require_relative 'client'
|
5
|
+
|
6
|
+
class DistributedPress
|
7
|
+
module V1
|
8
|
+
module Social
|
9
|
+
# Manages the actor's outbox on the Social Inbox
|
10
|
+
class Outbox
|
11
|
+
# @return [DistributedPress::V1::Social::Client]
|
12
|
+
attr_reader :client
|
13
|
+
|
14
|
+
# @return [String]
|
15
|
+
attr_reader :actor
|
16
|
+
|
17
|
+
# @param :client [DistributedPress::V1::Social::Client]
|
18
|
+
# @param :actor [String]
|
19
|
+
def initialize(client:, actor:)
|
20
|
+
@client = client
|
21
|
+
@actor = actor
|
22
|
+
end
|
23
|
+
|
24
|
+
# Send an activity to the actor's outbox. The Social Inbox will
|
25
|
+
# take care of sending it to the audiences.
|
26
|
+
#
|
27
|
+
# @param :activity [Hash]
|
28
|
+
# @return [HTTParty::Response]
|
29
|
+
def post(activity:)
|
30
|
+
client.post(endpoint: endpoint, body: activity)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get an activity queued on the outbox
|
34
|
+
#
|
35
|
+
# @param :id [String] Activity ID
|
36
|
+
# @return [HTTParty::Response]
|
37
|
+
def get(id:)
|
38
|
+
client.get(endpoint: "#{endpoint}/#{URI.encode_uri_component(id)}")
|
39
|
+
end
|
40
|
+
|
41
|
+
# Outbox
|
42
|
+
#
|
43
|
+
# @return [String]
|
44
|
+
def endpoint
|
45
|
+
@endpoint ||= "/v1/#{actor}/outbox"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class DistributedPress
|
4
|
+
module V1
|
5
|
+
module Social
|
6
|
+
# A lazy loaded reference to a remote object that can access its
|
7
|
+
# attributes directly.
|
8
|
+
class Reference
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
# @return [String]
|
12
|
+
attr_reader :uri
|
13
|
+
|
14
|
+
# @return [DistributedPress::V1::Social::Dereferencer]
|
15
|
+
attr_reader :dereferencer
|
16
|
+
|
17
|
+
# @param :uri [String]
|
18
|
+
# @param :dereferencer [DistributedPress::V1::Social::Dereferencer]
|
19
|
+
def initialize(uri:, dereferencer:)
|
20
|
+
@uri = uri
|
21
|
+
@dereferencer = dereferencer
|
22
|
+
end
|
23
|
+
|
24
|
+
# Fetches the remote object once
|
25
|
+
#
|
26
|
+
# @return [HTTParty::Response]
|
27
|
+
def object
|
28
|
+
@object ||= dereferencer.get(uri: uri)
|
29
|
+
end
|
30
|
+
|
31
|
+
def inspect
|
32
|
+
"#{self.class.name}(#{uri})"
|
33
|
+
end
|
34
|
+
|
35
|
+
def_delegators :object, :[], :dig, :to_h, :to_json
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class DistributedPress
|
4
|
+
module V1
|
5
|
+
module Social
|
6
|
+
# An object with external references
|
7
|
+
class ReferencedObject
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
REFERENTIABLE_ATTRIBUTES =
|
11
|
+
%w[
|
12
|
+
actor
|
13
|
+
owner
|
14
|
+
attributedTo
|
15
|
+
cc
|
16
|
+
inReplyTo
|
17
|
+
object
|
18
|
+
replies
|
19
|
+
to
|
20
|
+
publicKey
|
21
|
+
|
22
|
+
alsoKnownAs
|
23
|
+
devices
|
24
|
+
featured
|
25
|
+
featuredTags
|
26
|
+
followers
|
27
|
+
following
|
28
|
+
inbox
|
29
|
+
movedTo
|
30
|
+
outbox
|
31
|
+
|
32
|
+
first
|
33
|
+
items
|
34
|
+
next
|
35
|
+
orderedItems
|
36
|
+
partOf
|
37
|
+
prev
|
38
|
+
].freeze
|
39
|
+
|
40
|
+
attr_reader :object
|
41
|
+
attr_reader :dereferencer
|
42
|
+
attr_reader :referenced
|
43
|
+
|
44
|
+
def_delegators :referenced, :[], :dig, :to_h, :to_json
|
45
|
+
|
46
|
+
def initialize(object:, dereferencer:)
|
47
|
+
@object = object
|
48
|
+
@dereferencer = dereferencer
|
49
|
+
@referenced = HTTParty::ModuleInheritableAttributes.hash_deep_dup(object)
|
50
|
+
reference_object! referenced
|
51
|
+
end
|
52
|
+
|
53
|
+
def _dump(_)
|
54
|
+
Marshal.dump([object, dereferencer])
|
55
|
+
end
|
56
|
+
|
57
|
+
def self._load(array)
|
58
|
+
object, dereferencer = Marshal.load(array)
|
59
|
+
|
60
|
+
new(object: object, dereferencer: dereferencer)
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def reference_object!(object)
|
66
|
+
REFERENTIABLE_ATTRIBUTES.each do |attribute|
|
67
|
+
next unless object.key? attribute
|
68
|
+
|
69
|
+
case object[attribute]
|
70
|
+
when Array
|
71
|
+
object[attribute].map! do |o|
|
72
|
+
case o
|
73
|
+
when Hash
|
74
|
+
reference_object!(o)
|
75
|
+
o
|
76
|
+
when String then dereferencer.references(dereferencer.uris(o))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
when Hash
|
80
|
+
reference_object!(object[attribute])
|
81
|
+
when String
|
82
|
+
object[attribute] = dereferencer.references(dereferencer.uris(object[attribute]))
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-schema'
|
4
|
+
require_relative '../../../../dry/schema/processor_decorator'
|
5
|
+
require_relative '../../../../dry/schema/result_decorator'
|
6
|
+
|
7
|
+
class DistributedPress
|
8
|
+
module V1
|
9
|
+
module Social
|
10
|
+
# WebhookSchema
|
11
|
+
module Schemas
|
12
|
+
class Webhook < Dry::Schema::JSON
|
13
|
+
METHODS = %w[GET POST PUT DELETE].freeze
|
14
|
+
|
15
|
+
define do
|
16
|
+
required(:url).value(:uri_rfc3986?)
|
17
|
+
required(:method).value(included_in?: METHODS)
|
18
|
+
required(:headers).hash
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class DistributedPress
|
4
|
+
module V1
|
5
|
+
module Social
|
6
|
+
# Headers need to be signed by knowing the HTTP method (verb) and
|
7
|
+
# path. Since we're using caching, we need to change the
|
8
|
+
# signature after it's changed.
|
9
|
+
class SignedHeaders < Hash
|
10
|
+
# Signing algorithm
|
11
|
+
#
|
12
|
+
# @todo is it possible to use other algorithms?
|
13
|
+
ALGORITHM = 'rsa-sha256'
|
14
|
+
|
15
|
+
# Required by HTTP Signatures
|
16
|
+
REQUEST_TARGET = '(request-target)'
|
17
|
+
|
18
|
+
# Headers included in the signature
|
19
|
+
# rubocop:disable Style/MutableConstant
|
20
|
+
SIGNABLE_HEADERS = [REQUEST_TARGET, 'Host', 'Date', 'Digest']
|
21
|
+
# rubocop:enable Style/MutableConstant
|
22
|
+
|
23
|
+
# @return [HTTParty::Request]
|
24
|
+
attr_accessor :request
|
25
|
+
|
26
|
+
# @return [OpenSSL::PKey::RSA]
|
27
|
+
attr_accessor :private_key
|
28
|
+
|
29
|
+
# @return [String]
|
30
|
+
attr_accessor :public_key_url
|
31
|
+
|
32
|
+
# Takes advantage of HTTParty::Request.setup_raw_request running
|
33
|
+
# to_hash on the headers, so we sign as soon as we're ready to
|
34
|
+
# send the request.
|
35
|
+
#
|
36
|
+
# @return [Hash]
|
37
|
+
def to_hash
|
38
|
+
request_target!
|
39
|
+
sign_headers!
|
40
|
+
|
41
|
+
# xxx: converts to an actual Hash to facilitate marshaling
|
42
|
+
super.to_h
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# @return [String]
|
48
|
+
def verb
|
49
|
+
request.http_method.name.split('::').last.downcase
|
50
|
+
end
|
51
|
+
|
52
|
+
def request_target!
|
53
|
+
self[REQUEST_TARGET] = "#{verb} #{request.path}"
|
54
|
+
end
|
55
|
+
|
56
|
+
# HTTP Signatures
|
57
|
+
#
|
58
|
+
# @see {https://docs.joinmastodon.org/spec/security/}
|
59
|
+
# @param :headers [Hash]
|
60
|
+
def sign_headers!
|
61
|
+
self['Signature'] = {
|
62
|
+
'keyId' => public_key_url,
|
63
|
+
'algorithm' => ALGORITHM,
|
64
|
+
'headers' => signable_headers,
|
65
|
+
'signature' => signed_headers
|
66
|
+
}.map do |key, value|
|
67
|
+
"#{key}=\"#{value}\""
|
68
|
+
end.join(',')
|
69
|
+
|
70
|
+
delete REQUEST_TARGET
|
71
|
+
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
|
75
|
+
# List of headers to be signed, removing headers that don't
|
76
|
+
# exist on the request.
|
77
|
+
#
|
78
|
+
# @return [String]
|
79
|
+
def signable_headers
|
80
|
+
(SIGNABLE_HEADERS & keys).join(' ').downcase
|
81
|
+
end
|
82
|
+
|
83
|
+
# Sign headers
|
84
|
+
#
|
85
|
+
# @return [String]
|
86
|
+
def signed_headers
|
87
|
+
Base64.strict_encode64(
|
88
|
+
private_key.sign(
|
89
|
+
OpenSSL::Digest.new('SHA256'),
|
90
|
+
signature_content
|
91
|
+
)
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Generates a string to be signed
|
96
|
+
#
|
97
|
+
# @return [String]
|
98
|
+
def signature_content
|
99
|
+
slice(*SIGNABLE_HEADERS).map do |key, value|
|
100
|
+
"#{key.downcase}: #{value}"
|
101
|
+
end.join("\n")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/schema/processor'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Schema
|
7
|
+
# Include the processor as Result option so we can deserialize
|
8
|
+
# later.
|
9
|
+
module ProcessorDecorator
|
10
|
+
def self.included(base)
|
11
|
+
base.class_eval do
|
12
|
+
# Add the processor to the result so we can deserialize it
|
13
|
+
# later
|
14
|
+
def call(input)
|
15
|
+
Result.new(input.dup, message_compiler: message_compiler, processor: self) do |result|
|
16
|
+
steps.call(result)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
Dry::Schema::Processor.include Dry::Schema::ProcessorDecorator
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/schema/processor'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Schema
|
7
|
+
# This decorator allows to serialize Dry::Schema::Result objects
|
8
|
+
# by storing the data and processor so they can be run again during
|
9
|
+
# deserialization.
|
10
|
+
module ResultDecorator
|
11
|
+
def self.included(base)
|
12
|
+
base.class_eval do
|
13
|
+
option :processor
|
14
|
+
|
15
|
+
def _dump(_)
|
16
|
+
Marshal.dump(
|
17
|
+
{
|
18
|
+
processor: processor.class,
|
19
|
+
data: to_h
|
20
|
+
}
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self._load(data)
|
25
|
+
data = Marshal.load(data)
|
26
|
+
|
27
|
+
data[:processor].new.call(data[:data])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
Dry::Schema::Result.include Dry::Schema::ResultDecorator
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: distributed-press-api-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- f
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-03-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: addressable
|
@@ -72,6 +72,20 @@ dependencies:
|
|
72
72
|
- - "~>"
|
73
73
|
- !ruby/object:Gem::Version
|
74
74
|
version: '0.18'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: httparty-cache
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: 0.0.6
|
82
|
+
type: :runtime
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 0.0.6
|
75
89
|
- !ruby/object:Gem::Dependency
|
76
90
|
name: json
|
77
91
|
requirement: !ruby/object:Gem::Requirement
|
@@ -224,6 +238,20 @@ dependencies:
|
|
224
238
|
- - ">="
|
225
239
|
- !ruby/object:Gem::Version
|
226
240
|
version: '0'
|
241
|
+
- !ruby/object:Gem::Dependency
|
242
|
+
name: ruby-brs
|
243
|
+
requirement: !ruby/object:Gem::Requirement
|
244
|
+
requirements:
|
245
|
+
- - ">="
|
246
|
+
- !ruby/object:Gem::Version
|
247
|
+
version: '0'
|
248
|
+
type: :development
|
249
|
+
prerelease: false
|
250
|
+
version_requirements: !ruby/object:Gem::Requirement
|
251
|
+
requirements:
|
252
|
+
- - ">="
|
253
|
+
- !ruby/object:Gem::Version
|
254
|
+
version: '0'
|
227
255
|
- !ruby/object:Gem::Dependency
|
228
256
|
name: webmock
|
229
257
|
requirement: !ruby/object:Gem::Requirement
|
@@ -279,9 +307,21 @@ files:
|
|
279
307
|
- lib/distributed_press/v1/schemas/token_payload.rb
|
280
308
|
- lib/distributed_press/v1/schemas/update_site.rb
|
281
309
|
- lib/distributed_press/v1/social.rb
|
310
|
+
- lib/distributed_press/v1/social/allowlist.rb
|
311
|
+
- lib/distributed_press/v1/social/blocklist.rb
|
282
312
|
- lib/distributed_press/v1/social/client.rb
|
313
|
+
- lib/distributed_press/v1/social/dereferencer.rb
|
314
|
+
- lib/distributed_press/v1/social/hook.rb
|
315
|
+
- lib/distributed_press/v1/social/inbox.rb
|
316
|
+
- lib/distributed_press/v1/social/outbox.rb
|
317
|
+
- lib/distributed_press/v1/social/reference.rb
|
318
|
+
- lib/distributed_press/v1/social/referenced_object.rb
|
319
|
+
- lib/distributed_press/v1/social/schemas/webhook.rb
|
320
|
+
- lib/distributed_press/v1/social/signed_headers.rb
|
283
321
|
- lib/distributed_press/v1/token.rb
|
284
322
|
- lib/distributed_press/version.rb
|
323
|
+
- lib/dry/schema/processor_decorator.rb
|
324
|
+
- lib/dry/schema/result_decorator.rb
|
285
325
|
- lib/jekyll-distributed-press-v0.rb
|
286
326
|
homepage: https://0xacab.org/sutty/distributed-press-api-client
|
287
327
|
licenses:
|