distributed-press-api-client 0.3.1 → 0.4.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 +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:
|