shep 0.1.0.pre.alpha0 → 0.2.0.pre.alpha0
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/README.md +5 -1
- data/Rakefile +1 -0
- data/doc/Shep/Entity/Account.html +22 -37
- data/doc/Shep/Entity/Context.html +8 -9
- data/doc/Shep/Entity/CustomEmoji.html +11 -15
- data/doc/Shep/Entity/MediaAttachment.html +13 -19
- data/doc/Shep/Entity/Notification.html +11 -15
- data/doc/Shep/Entity/Status.html +34 -61
- data/doc/Shep/Entity/StatusSource.html +9 -11
- data/doc/Shep/Entity/Status_Application.html +11 -10
- data/doc/Shep/Entity/Status_Mention.html +10 -13
- data/doc/Shep/Entity/Status_Tag.html +8 -9
- data/doc/Shep/Entity.html +156 -141
- data/doc/Shep/Error/Caller.html +4 -4
- data/doc/Shep/Error/Http.html +13 -13
- data/doc/Shep/Error/RateLimit.html +176 -0
- data/doc/Shep/Error/Remote.html +3 -4
- data/doc/Shep/Error/Server.html +5 -4
- data/doc/Shep/Error/Type.html +6 -8
- data/doc/Shep/Error.html +5 -5
- data/doc/Shep/Session.html +895 -570
- data/doc/Shep.html +20 -5
- data/doc/_index.html +10 -3
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +52 -33
- data/doc/file_list.html +1 -1
- data/doc/index.html +117 -239
- data/doc/method_list.html +9 -1
- data/doc/top-level-namespace.html +2 -2
- data/lib/shep/entity_base.rb +6 -1
- data/lib/shep/exceptions.rb +5 -0
- data/lib/shep/session.rb +292 -146
- data/lib/shep/version.rb +4 -0
- data/lib/shep.rb +1 -1
- data/run_rake_test.example.sh +12 -5
- data/shep.gemspec +4 -2
- data/spec/session_reader_1_unauth_spec.rb +20 -17
- data/spec/session_reader_2_auth_spec.rb +17 -19
- data/spec/session_writer_spec.rb +4 -11
- data/spec/session_zzz_tricky_spec.rb +136 -0
- data/spec/spec_helper.rb +12 -3
- metadata +12 -9
data/lib/shep/session.rb
CHANGED
@@ -11,8 +11,9 @@ module Shep
|
|
11
11
|
|
12
12
|
# Represents a connection to a Mastodon (or equivalent) server.
|
13
13
|
#
|
14
|
-
# @attr_reader [Logger] logger
|
15
|
-
# @attr_reader [String] host
|
14
|
+
# @attr_reader [Logger] logger The logger object
|
15
|
+
# @attr_reader [String] host The Server's hostname
|
16
|
+
# @attr_reader [String] user_agent User-Agent string; frozen
|
16
17
|
#
|
17
18
|
# ## Conventions
|
18
19
|
#
|
@@ -24,29 +25,97 @@ module Shep
|
|
24
25
|
# each item in turn and the block's result is ignored. Otherwise,
|
25
26
|
# it returns an `Enumerator` which can be used in the usual ways.
|
26
27
|
#
|
27
|
-
#
|
28
|
-
# for example,
|
28
|
+
# Some examples:
|
29
29
|
#
|
30
|
-
#
|
30
|
+
# # Evaluate a block on each status
|
31
|
+
# session.each_status(account) { |status| do_thing(status) }
|
31
32
|
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
# the
|
33
|
+
# # Retrieve the last 100 statuses in an array
|
34
|
+
# statuses = session.each_status(account, limit: 100).to_a
|
35
|
+
#
|
36
|
+
# # Retrieve the last 200 statuses via an enumerator and do
|
37
|
+
# # extra transformation on the result before collecting
|
38
|
+
# # them in an array.
|
39
|
+
# statuses = session.each_status(account, limit: 200)
|
40
|
+
# .select{|status| BESTIES.include? status.account.username }
|
41
|
+
# .map{|status| status.id}
|
42
|
+
# .to_a
|
43
|
+
#
|
44
|
+
# The actual web API "paginates" the output. That is, it returns
|
45
|
+
# the first 40 (or so) items and then provides a link to the next
|
46
|
+
# chunk. Shep's `each_*` methods handle this for you automatically.
|
47
|
+
# This means that unless you use `limit:`, the `each_*` methods will
|
48
|
+
# retrieve **all** available items, at least until you reach the
|
49
|
+
# rate limit (see below).
|
50
|
+
#
|
51
|
+
# Note that it is safe to leave an `each_*` methods block with
|
52
|
+
# `break`, `return`, an exception, or any other such mechanism.
|
36
53
|
#
|
37
54
|
# The remaining Mastodon API methods will in some way modify the
|
38
55
|
# state of the server and return an Entity subinstance on success.
|
39
56
|
#
|
40
57
|
# All API calls throw an exception on failure.
|
58
|
+
#
|
59
|
+
# ## Rate Limits
|
60
|
+
#
|
61
|
+
# Mastodon servers restrict the number of times you can use a
|
62
|
+
# specific endpoint within a time period as a way to prevent abuse.
|
63
|
+
# Shep provides several tools for handling these limits gracefully.
|
64
|
+
#
|
65
|
+
# 1. The method {rate_limit} will return a Struct that tells you how
|
66
|
+
# many requests you have left and when the count is reset.
|
67
|
+
#
|
68
|
+
# 2. If a rate limit is exceeded, the method will throw an
|
69
|
+
# {Error::RateLimit} exception instead of an ordinary
|
70
|
+
# {Error::Http} exception.
|
71
|
+
#
|
72
|
+
# 3. If the Session is created with argument `rate_limit_retry:` set
|
73
|
+
# to true, the Session will instead wait out the reset time and
|
74
|
+
# try again.
|
75
|
+
#
|
76
|
+
# If you enable the wait-and-retry mechanism, you can also provide a
|
77
|
+
# hook function (i.e. a thing that responds to `call`) via
|
78
|
+
# constructor argument `retry_hook:`. This is called with one
|
79
|
+
# argument, the result of {rate_limit} for the limited API endpoint,
|
80
|
+
# immediately before Shep starts waiting for the limit to reset.
|
81
|
+
#
|
82
|
+
# The built-in wait time takes the callback's execution time into
|
83
|
+
# account so it's possible to use the callback to do your own
|
84
|
+
# waiting and use that time more productively.
|
85
|
+
#
|
86
|
+
# Alternately, all of the `each_*` methods have a `limit:` parameter
|
87
|
+
# so it's easy to avoid making too many API calls and many have a
|
88
|
+
# `max_id:` parameter that allows you to continue where you left off.
|
89
|
+
#
|
41
90
|
class Session
|
42
|
-
attr_reader :logger, :host
|
91
|
+
attr_reader :logger, :host, :user_agent
|
43
92
|
|
44
93
|
# Initialize a new {Session}.
|
45
94
|
#
|
46
|
-
# @param host
|
47
|
-
# @param token
|
48
|
-
#
|
49
|
-
# @param
|
95
|
+
# @param host [String] Hostname of the server
|
96
|
+
# @param token [String] Bearer token; optional
|
97
|
+
#
|
98
|
+
# @param user_agent [String] User-Agent string to use
|
99
|
+
# @param ua_comment [String] Comment part of User-Agent string
|
100
|
+
#
|
101
|
+
# @param rate_limit_retry [Boolean] Handle request limits by waiting
|
102
|
+
# for the count to reset and trying
|
103
|
+
# again.
|
104
|
+
#
|
105
|
+
# @param retry_hook [Proc] One-argument hook function to call
|
106
|
+
# before waiting for the rate limit
|
107
|
+
# to reset
|
108
|
+
#
|
109
|
+
# @param logger [Logger] The logger or mode; optional
|
110
|
+
# @param debug_http [Boolean] Enable `Net::HTTP` debugging;
|
111
|
+
# **insecure!**
|
112
|
+
#
|
113
|
+
# By default, the User-Agent header is set to the gem's
|
114
|
+
# identifier, but may be overridden with the `user_agent`
|
115
|
+
# parameter. It is your responsibility to make sure it is
|
116
|
+
# formatted correctly. You can also append comment text to the
|
117
|
+
# given User-Agent string with `ua_comment`; this lets you add a
|
118
|
+
# comment to the default text.
|
50
119
|
#
|
51
120
|
# Parameter `logger` may be a `Logger` object, `nil`, or a
|
52
121
|
# `Symbol` whose value is the name of one of the supported log
|
@@ -56,17 +125,37 @@ module Shep
|
|
56
125
|
#
|
57
126
|
# If `debug_http` is true, compression is disabled and the
|
58
127
|
# transactions are sent to `STDERR` via
|
59
|
-
# `Net::HTTP.set_debug_output`.
|
60
|
-
#
|
128
|
+
# `Net::HTTP.set_debug_output`.
|
129
|
+
#
|
130
|
+
# **WARNING:** this opens a serious security hole and should not
|
131
|
+
# be used in production.
|
61
132
|
def initialize(host:,
|
62
|
-
token:
|
63
|
-
|
64
|
-
|
133
|
+
token: nil,
|
134
|
+
user_agent: "ShepRubyGem/#{Shep::Version}",
|
135
|
+
ua_comment: nil,
|
136
|
+
|
137
|
+
rate_limit_retry: false,
|
138
|
+
retry_hook: nil,
|
139
|
+
|
140
|
+
logger: nil,
|
141
|
+
debug_http: false)
|
65
142
|
@host = host
|
66
143
|
@token = token
|
67
144
|
@logger = init_logger(logger)
|
68
|
-
|
145
|
+
|
146
|
+
@user_agent = user_agent
|
147
|
+
@user_agent += " #{ua_comment}" if ua_comment
|
148
|
+
@user_agent.freeze
|
149
|
+
|
150
|
+
@rate_limit_retry = rate_limit_retry
|
151
|
+
@retry_hook = retry_hook
|
152
|
+
|
69
153
|
@debug_http = debug_http
|
154
|
+
|
155
|
+
@rate_limit = Struct.new(:limit, :remaining, :reset).new
|
156
|
+
|
157
|
+
raise Error::Caller.new("retry_hook: must a callable or nil") unless
|
158
|
+
@retry_hook == nil || @retry_hook.respond_to?(:call)
|
70
159
|
end
|
71
160
|
|
72
161
|
private
|
@@ -88,7 +177,7 @@ module Shep
|
|
88
177
|
|
89
178
|
public
|
90
179
|
|
91
|
-
# Return the rate limit information
|
180
|
+
# Return the rate limit information from the last REST request.
|
92
181
|
#
|
93
182
|
# The result is a Struct with the following fields:
|
94
183
|
#
|
@@ -101,6 +190,10 @@ module Shep
|
|
101
190
|
# times within 5 minutes but no more than 30 media uploads are
|
102
191
|
# allowed within a 30 minute time period.
|
103
192
|
#
|
193
|
+
# Note also that some Shep methods will perform multiple API
|
194
|
+
# requests; this is only ever the rate limit information from the
|
195
|
+
# latest of these.
|
196
|
+
#
|
104
197
|
# @see https://docs.joinmastodon.org/api/rate-limits/
|
105
198
|
#
|
106
199
|
# @return [Struct.new(:limit, :remaining, :reset)]
|
@@ -150,14 +243,19 @@ module Shep
|
|
150
243
|
#
|
151
244
|
# The username must belong to a user on the current server.
|
152
245
|
#
|
153
|
-
# @param handle [String] the account's username
|
246
|
+
# @param handle [String] the account's username with or without the
|
154
247
|
# leading '@' character (e.g. @benoitmandelbot)
|
155
248
|
#
|
156
|
-
#
|
249
|
+
#
|
250
|
+
# @return [Entity::Account, nil] The Account or nil if it can't be found.
|
157
251
|
#
|
158
252
|
# @see https://docs.joinmastodon.org/methods/accounts/#get
|
159
253
|
def fetch_account_by_username(handle)
|
160
254
|
return rest_get("accounts/lookup", Entity::Account, {acct: handle})
|
255
|
+
rescue Error::Http => oopsie
|
256
|
+
# As a special case, return nil if the lookup fails
|
257
|
+
return nil if oopsie.response.is_a?(Net::HTTPNotFound)
|
258
|
+
raise oopsie
|
161
259
|
end
|
162
260
|
|
163
261
|
# Fetch an individual notification by ID.
|
@@ -233,18 +331,18 @@ module Shep
|
|
233
331
|
|
234
332
|
if !refetch && File.exist?(outfile)
|
235
333
|
@logger.info "Found '#{outfile}'; skipping."
|
236
|
-
success = true
|
237
334
|
else
|
238
335
|
tmp = File.join(media_dir, SecureRandom.uuid + '.tmp')
|
239
|
-
|
240
|
-
|
336
|
+
begin
|
337
|
+
basic_get_binary(ma.url, tmp)
|
241
338
|
FileUtils.mv(tmp, outfile)
|
242
|
-
|
339
|
+
rescue Error::Http => e
|
243
340
|
FileUtils.rm(tmp, force: true)
|
341
|
+
raise e
|
244
342
|
end
|
245
343
|
end
|
246
344
|
|
247
|
-
media[ma.url.to_s] =
|
345
|
+
media[ma.url.to_s] = outfile
|
248
346
|
}
|
249
347
|
|
250
348
|
return [status, media]
|
@@ -300,6 +398,8 @@ module Shep
|
|
300
398
|
# @param limit [Integer] Maximum number of accounts to
|
301
399
|
# retrieve
|
302
400
|
#
|
401
|
+
# @param max_id [String] retrieve results older than this ID.
|
402
|
+
#
|
303
403
|
# @param only_media [Boolean] If true, filter for statuses
|
304
404
|
# with media
|
305
405
|
#
|
@@ -321,6 +421,7 @@ module Shep
|
|
321
421
|
# @see https://docs.joinmastodon.org/methods/accounts/#statuses
|
322
422
|
def each_status(account_id,
|
323
423
|
limit: nil,
|
424
|
+
max_id: "",
|
324
425
|
only_media: false,
|
325
426
|
exclude_replies: false,
|
326
427
|
exclude_reblogs: false,
|
@@ -337,6 +438,8 @@ module Shep
|
|
337
438
|
#
|
338
439
|
# @param limit [Integer] Max. items to retrieve.
|
339
440
|
#
|
441
|
+
# @param max_id [String] retrieve results older than this ID.
|
442
|
+
#
|
340
443
|
# @param local [Boolean] Retrieve only local statuses
|
341
444
|
#
|
342
445
|
# @param remote [Boolean] Retrieve only remote statuses
|
@@ -351,6 +454,7 @@ module Shep
|
|
351
454
|
#
|
352
455
|
# @see https://docs.joinmastodon.org/methods/timelines/#public
|
353
456
|
def each_public_status(limit: nil,
|
457
|
+
max_id: "",
|
354
458
|
local: false,
|
355
459
|
remote: false,
|
356
460
|
only_media: false,
|
@@ -373,6 +477,8 @@ module Shep
|
|
373
477
|
#
|
374
478
|
# @param limit [Integer] maximum number of items to retrieve
|
375
479
|
#
|
480
|
+
# @param max_id [String] return results older than this ID.
|
481
|
+
#
|
376
482
|
# @param local [Boolean] retrieve only local statuses
|
377
483
|
#
|
378
484
|
# @param remote [Boolean] retrieve only remote statuses
|
@@ -392,6 +498,7 @@ module Shep
|
|
392
498
|
# @see https://docs.joinmastodon.org/methods/timelines/#tag
|
393
499
|
def each_tag_status(hashtag_s,
|
394
500
|
limit: nil,
|
501
|
+
max_id: "",
|
395
502
|
local: false,
|
396
503
|
remote: false,
|
397
504
|
only_media: false,
|
@@ -422,10 +529,16 @@ module Shep
|
|
422
529
|
#
|
423
530
|
# Requires token.
|
424
531
|
#
|
425
|
-
# @param limit
|
426
|
-
#
|
427
|
-
# @param
|
428
|
-
#
|
532
|
+
# @param limit [Integer] maximum number of items to retrieve
|
533
|
+
#
|
534
|
+
# @param max_id [String] retrieve results older than this ID.
|
535
|
+
#
|
536
|
+
# @param local [Boolean] retrieve only local statuses
|
537
|
+
#
|
538
|
+
# @param remote [Boolean] retrieve only remote statuses
|
539
|
+
#
|
540
|
+
# @param only_media [Boolean] retrieve only media status
|
541
|
+
#
|
429
542
|
#
|
430
543
|
# @yield [item]
|
431
544
|
#
|
@@ -437,6 +550,7 @@ module Shep
|
|
437
550
|
# @see https://docs.joinmastodon.org/methods/timelines/#home
|
438
551
|
def each_home_status(limit: nil,
|
439
552
|
local: false,
|
553
|
+
max_id: "",
|
440
554
|
remote: false,
|
441
555
|
only_media: false,
|
442
556
|
&block)
|
@@ -488,6 +602,18 @@ module Shep
|
|
488
602
|
|
489
603
|
# Retrieve each notification.
|
490
604
|
#
|
605
|
+
# Requires a bearer token.
|
606
|
+
#
|
607
|
+
# Notification types are indicated by of the following symbols:
|
608
|
+
#
|
609
|
+
# `:mention`, `:status`, `:reblog`, `:follow`, `:follow_request`
|
610
|
+
# `:favourite`, `:poll`, `:update`, `:admin.sign_up`, or
|
611
|
+
# `:admin.report`
|
612
|
+
#
|
613
|
+
# This method will throw an `Error::Caller` exception if an
|
614
|
+
# unknown value is used.
|
615
|
+
#
|
616
|
+
#
|
491
617
|
# @param types [Array<String>] list of notifications types to
|
492
618
|
# enumerate; others are ignoredn
|
493
619
|
#
|
@@ -514,9 +640,13 @@ module Shep
|
|
514
640
|
favourite poll update admin.sign_up
|
515
641
|
admin.report}
|
516
642
|
|
517
|
-
#
|
518
|
-
types.
|
519
|
-
|
643
|
+
# Remove duplicates and convert strings to symbols
|
644
|
+
[types, exclude_types].each{|param|
|
645
|
+
param.map!{|item| item.intern}
|
646
|
+
param.uniq!
|
647
|
+
}
|
648
|
+
|
649
|
+
# Now, ensure there are no incorrect notification types.
|
520
650
|
(types + exclude_types).each{|filter|
|
521
651
|
assert("Unknown notification type: #{filter}") {
|
522
652
|
allowed_notifications.include?(filter.intern)
|
@@ -550,6 +680,8 @@ module Shep
|
|
550
680
|
#
|
551
681
|
# @param language [String] ISO language code
|
552
682
|
#
|
683
|
+
# @return [Entity::Status] The new status.
|
684
|
+
#
|
553
685
|
# @see https://docs.joinmastodon.org/methods/statuses/#create
|
554
686
|
def post_status(text,
|
555
687
|
visibility: :private,
|
@@ -669,12 +801,14 @@ module Shep
|
|
669
801
|
return nil
|
670
802
|
end
|
671
803
|
|
672
|
-
|
804
|
+
|
673
805
|
|
674
806
|
#
|
675
|
-
# High-level REST support
|
807
|
+
# High(ish)-level REST support
|
676
808
|
#
|
677
809
|
|
810
|
+
private
|
811
|
+
|
678
812
|
# Extract all of the keyword arguments and their values from the
|
679
813
|
# caller's context. (The context is 'cbinding', which must be a
|
680
814
|
# call to method 'cmethod'.)
|
@@ -711,7 +845,7 @@ module Shep
|
|
711
845
|
|
712
846
|
def rest_delete(path, result_klass)
|
713
847
|
uri = rest_uri(path, {})
|
714
|
-
result_obj, _ = basic_rest_get_or_delete(uri,
|
848
|
+
result_obj, _ = basic_rest_get_or_delete(uri, is_delete: true)
|
715
849
|
return result_klass.from(result_obj)
|
716
850
|
end
|
717
851
|
|
@@ -766,7 +900,7 @@ module Shep
|
|
766
900
|
|
767
901
|
|
768
902
|
#
|
769
|
-
# Low-level REST support
|
903
|
+
# Low(ish)-level REST support
|
770
904
|
#
|
771
905
|
|
772
906
|
# Given a hash of query arguments, return an array of key-value
|
@@ -804,51 +938,91 @@ module Shep
|
|
804
938
|
return URI("https://#{@host}/api/#{version}/#{path}#{args}")
|
805
939
|
end
|
806
940
|
|
807
|
-
def
|
941
|
+
def parse_link_header(hdr)
|
942
|
+
result = {}
|
943
|
+
return result unless hdr # could be nil
|
944
|
+
|
945
|
+
for link in hdr.split(', ')
|
946
|
+
md = link.match(/^<([^>]+)>; rel="([^"]+)"/)
|
947
|
+
assert{md}
|
948
|
+
|
949
|
+
result[ md[2] ] = md[1]
|
950
|
+
end
|
951
|
+
|
952
|
+
return result
|
953
|
+
end
|
954
|
+
|
955
|
+
# Perform a GET or DELETE operation. (They have mostly the same
|
956
|
+
# structure, so we combine the functionality here.)
|
957
|
+
def basic_rest_get_or_delete(url, is_delete: false)
|
808
958
|
headers = headers_for(:get)
|
809
959
|
|
810
|
-
|
811
|
-
|
812
|
-
|
960
|
+
request = is_delete ?
|
961
|
+
Net::HTTP::Delete.new(url, headers) :
|
962
|
+
Net::HTTP::Get.new(url, headers)
|
813
963
|
|
814
|
-
|
815
|
-
raise Error::Http.new(response) if response.is_a? Net::HTTPClientError
|
964
|
+
response = http_operation(request)
|
816
965
|
|
817
|
-
|
966
|
+
result = JSON.parse(response.body)
|
818
967
|
|
819
|
-
if result.is_a?(Hash) &&
|
820
|
-
raise Error::Server.new(
|
968
|
+
if result.is_a?(Hash) && result.has_key?("error")
|
969
|
+
raise Error::Server.new(result["error"])
|
821
970
|
end
|
822
971
|
|
823
972
|
link = parse_link_header(response["link"])
|
824
973
|
|
825
|
-
return [
|
974
|
+
return [result, link]
|
826
975
|
rescue JSON::JSONError => e
|
827
976
|
raise Error::Remote.new("Error parsing result JSON: #{e}")
|
828
977
|
end
|
829
978
|
|
830
|
-
|
831
|
-
|
832
|
-
|
979
|
+
# Retrieve the resource (assumed to be binary) at 'uri' and write
|
980
|
+
# it to 'filename'. For now, returns false if the request
|
981
|
+
# returned an error code and true on success.
|
982
|
+
def basic_get_binary(url, filename)
|
983
|
+
request = Net::HTTP::Get.new(url, headers_for(:get))
|
833
984
|
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
) { |http|
|
839
|
-
next is_delete ?
|
840
|
-
http.delete(uri.path, headers) :
|
841
|
-
http.request_get(uri, headers)
|
842
|
-
}
|
985
|
+
@logger.debug("Output file is #{filename}")
|
986
|
+
File.open(filename, "wb") { |outfile| http_operation(request, outfile) }
|
987
|
+
@logger.debug("Done (#{filename})")
|
988
|
+
end
|
843
989
|
|
844
|
-
|
845
|
-
|
990
|
+
def basic_rest_post_or_put(url, formdata, is_put: false)
|
991
|
+
headers = headers_for(:post)
|
992
|
+
|
993
|
+
# Select request type
|
994
|
+
request = is_put ?
|
995
|
+
Net::HTTP::Put.new(url, headers) :
|
996
|
+
Net::HTTP::Post.new(url, headers)
|
997
|
+
|
998
|
+
# Set the parameters
|
999
|
+
enctype = 'multipart/form-data' if formdata.is_a?(Array)
|
1000
|
+
enctype = 'application/x-www-form-urlencoded' if
|
1001
|
+
formdata.is_a?(Hash)
|
1002
|
+
enctype or
|
1003
|
+
raise Error::Caller.new("Unknown formdate type: #{formdata.class}")
|
1004
|
+
request.set_form(formdata, enctype)
|
1005
|
+
|
1006
|
+
# Do the deed
|
1007
|
+
response = http_operation(request)
|
1008
|
+
|
1009
|
+
result = JSON.parse(response.body)
|
1010
|
+
|
1011
|
+
if result.is_a?(Hash) && result.has_key?("error")
|
1012
|
+
raise Error::Server.new(result["error"])
|
1013
|
+
end
|
1014
|
+
|
1015
|
+
return result
|
1016
|
+
rescue JSON::JSONError => e
|
1017
|
+
raise Error::Remote.new("Error parsing result JSON: #{e}")
|
846
1018
|
end
|
847
1019
|
|
848
1020
|
def headers_for(method)
|
849
1021
|
headers = {}
|
850
1022
|
headers["Authorization"] = "Bearer #{@token}" if @token
|
851
1023
|
|
1024
|
+
headers['User-Agent'] = @user_agent
|
1025
|
+
|
852
1026
|
if method == :post
|
853
1027
|
extras = {
|
854
1028
|
"Indempotency-Key": SecureRandom.uuid,
|
@@ -860,111 +1034,83 @@ module Shep
|
|
860
1034
|
return headers
|
861
1035
|
end
|
862
1036
|
|
863
|
-
def
|
864
|
-
|
1037
|
+
def http_operation(request, output_handle = nil)
|
1038
|
+
url = request.uri
|
865
1039
|
|
866
|
-
|
867
|
-
|
1040
|
+
while true
|
1041
|
+
http = Net::HTTP.new(url.hostname, url.port)
|
1042
|
+
http.use_ssl = (url.scheme == 'https')
|
868
1043
|
|
869
|
-
|
870
|
-
|
1044
|
+
if @debug_http
|
1045
|
+
http.set_debug_output(STDERR)
|
1046
|
+
request['Accept-Encoding'] = 'identity'
|
1047
|
+
end
|
871
1048
|
|
872
|
-
|
873
|
-
|
1049
|
+
http.start do |http|
|
1050
|
+
@logger.debug("Request #{request}; (token: #{!!@token})")
|
874
1051
|
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
1052
|
+
# We have to invoke 'request' with a block argument to get the
|
1053
|
+
# response because that's the only way we can get at it before
|
1054
|
+
# it's downloaded the entire response into RAM.
|
1055
|
+
http.request(request) do |response|
|
1056
|
+
@logger.debug("Response: #{response}")
|
880
1057
|
|
881
|
-
|
882
|
-
Net::HTTP.get_response(uri, headers) {|response|
|
883
|
-
@logger.debug("Response: #{response.code}")
|
1058
|
+
update_rate_limit(response)
|
884
1059
|
|
885
|
-
|
1060
|
+
#raise_http_exception_if_error(response)
|
886
1061
|
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
1062
|
+
if response.is_a?(Net::HTTPClientError)
|
1063
|
+
# Finish any body reading that may have been in
|
1064
|
+
# progress.
|
1065
|
+
response.read_body()
|
891
1066
|
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
}
|
899
|
-
@logger.debug("Done (#{filename})")
|
900
|
-
}
|
1067
|
+
# Special case: too many requests. We may throw an
|
1068
|
+
# exception or wait until it resets and try again.
|
1069
|
+
if response.is_a?(Net::HTTPTooManyRequests)
|
1070
|
+
handle_rate_limit_reached(response)
|
1071
|
+
next # if we get here, we're trying again
|
1072
|
+
end
|
901
1073
|
|
902
|
-
|
903
|
-
|
1074
|
+
raise Error::Http.new(response)
|
1075
|
+
end
|
904
1076
|
|
905
|
-
|
906
|
-
|
907
|
-
|
1077
|
+
# read_body will write the response body to output_handle if
|
1078
|
+
# it's a handle or keep it internally if it's nil.
|
1079
|
+
response.read_body(output_handle)
|
908
1080
|
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
result[ md[2] ] = md[1]
|
1081
|
+
return response
|
1082
|
+
end
|
1083
|
+
end
|
914
1084
|
end
|
915
|
-
|
916
|
-
return result
|
917
1085
|
end
|
918
1086
|
|
1087
|
+
def handle_rate_limit_reached(response)
|
1088
|
+
raise Error::RateLimit.new(response) unless @rate_limit_retry
|
919
1089
|
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
# Do the thing.
|
924
|
-
@logger.debug("Posting #{uri} (token: #{!!@token})")
|
925
|
-
response = http_post_form(uri, headers, formdata, is_put)
|
926
|
-
@logger.debug("Response: #{response}")
|
927
|
-
|
928
|
-
update_rate_limit(response)
|
1090
|
+
# Call the retry hook first.
|
1091
|
+
@retry_hook.call(self.rate_limit().dup) if @retry_hook
|
929
1092
|
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
raise Error::Server.new(result_obj["error"])
|
1093
|
+
# Now, wait out any remaining elapsed time.
|
1094
|
+
while true
|
1095
|
+
delay = (@rate_limit.reset - Time.now) + 2
|
1096
|
+
break if delay <= 0
|
1097
|
+
@logger.info "Sleeping for #{delay.round} seconds."
|
1098
|
+
sleep delay
|
937
1099
|
end
|
938
|
-
|
939
|
-
return result_obj
|
940
|
-
rescue JSON::JSONError => e
|
941
|
-
raise Error::Remote.new("Error parsing result JSON: #{e}")
|
942
1100
|
end
|
943
1101
|
|
944
|
-
def
|
945
|
-
|
946
|
-
Net::HTTP::Put.new(url, headers) :
|
947
|
-
Net::HTTP::Post.new(url, headers)
|
948
|
-
|
949
|
-
enctype = 'multipart/form-data' if formdata.is_a?(Array)
|
950
|
-
enctype = 'application/x-www-form-urlencoded' if
|
951
|
-
formdata.is_a?(Hash)
|
952
|
-
enctype or
|
953
|
-
raise Error::Caller.new("Unknown formdate type: #{formdata.class}")
|
954
|
-
request.set_form(formdata, enctype)
|
955
|
-
|
1102
|
+
def update_rate_limit(response)
|
1103
|
+
@rate_limit.limit = @rate_limit.remaining = @rate_limit.reset = nil
|
956
1104
|
|
957
|
-
|
958
|
-
|
1105
|
+
@rate_limit.limit = response['X-RateLimit-Limit'].to_i
|
1106
|
+
@rate_limit.remaining = response['X-RateLimit-Remaining'].to_i
|
959
1107
|
|
960
|
-
|
961
|
-
|
962
|
-
request['Accept-Encoding'] = 'identity'
|
963
|
-
end
|
1108
|
+
reset = response['X-RateLimit-Reset']
|
1109
|
+
@rate_limit.reset = Time.iso8601(reset) if reset
|
964
1110
|
|
965
|
-
|
966
|
-
http.request(request)
|
967
|
-
}
|
1111
|
+
@logger.debug "Rate limit: #{rate_limit_desc}"
|
968
1112
|
end
|
1113
|
+
|
969
1114
|
end
|
1115
|
+
|
970
1116
|
end
|
data/lib/shep/version.rb
ADDED