redd 0.8.4 → 0.8.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.hound.yml +2 -0
- data/.rspec +1 -1
- data/.travis.yml +1 -1
- data/lib/redd/api_client.rb +23 -13
- data/lib/redd/models/comment.rb +9 -7
- data/lib/redd/models/listing.rb +0 -6
- data/lib/redd/models/more_comments.rb +10 -14
- data/lib/redd/models/replyable.rb +1 -1
- data/lib/redd/models/submission.rb +19 -5
- data/lib/redd/models/subreddit.rb +17 -0
- data/lib/redd/models/wiki_page.rb +15 -1
- data/lib/redd/utilities/error_handler.rb +40 -19
- data/lib/redd/utilities/rate_limiter.rb +2 -2
- data/lib/redd/utilities/unmarshaller.rb +35 -13
- data/lib/redd/version.rb +1 -1
- data/redd.gemspec +3 -2
- metadata +21 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c41f2214b01198dd4f0a44f243bc73364af33b6c
|
4
|
+
data.tar.gz: a33587b7a36a41c11b3a4faa51814af24d86565f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ba2ecdabcf4c991710428b08e57437a00e0b5c85a0164d7786fe4363f65f51617e544519393bc48cae3672debaa0b8c2ea4743aae67ae1e23dedfee32e24dfe9
|
7
|
+
data.tar.gz: ef71ea37b3375b7328f9f6f6298d8cb867dda0aa38eec6b0480fcc12063b935a6fcfea99216d0bac5563577fd8a86c98697ef1a697ebfab09365a3e60390e3a5
|
data/.gitignore
CHANGED
data/.hound.yml
ADDED
data/.rspec
CHANGED
data/.travis.yml
CHANGED
data/lib/redd/api_client.rb
CHANGED
@@ -68,19 +68,16 @@ module Redd
|
|
68
68
|
ensure_access_is_valid
|
69
69
|
# Setup base API params and make request
|
70
70
|
api_params = { api_type: 'json', raw_json: 1 }.merge(params)
|
71
|
-
|
72
|
-
#
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
raise e if @failures > @max_retries
|
82
|
-
warn "Redd got a #{e.class.name} error (#{e.message}), retrying..."
|
83
|
-
retry
|
71
|
+
|
72
|
+
# This loop is retried @max_retries number of times until it succeeds
|
73
|
+
handle_retryable_errors do
|
74
|
+
response = @rate_limiter.after_limit { super(verb, path, params: api_params, **options) }
|
75
|
+
# Raise errors if encountered at the API level.
|
76
|
+
response_error = @error_handler.check_error(response, raw: raw)
|
77
|
+
raise response_error unless response_error.nil?
|
78
|
+
# All done, return the response
|
79
|
+
response
|
80
|
+
end
|
84
81
|
end
|
85
82
|
|
86
83
|
private
|
@@ -93,6 +90,19 @@ module Redd
|
|
93
90
|
refresh if @access.expired? && @auto_refresh
|
94
91
|
end
|
95
92
|
|
93
|
+
def handle_retryable_errors
|
94
|
+
response = yield
|
95
|
+
rescue Redd::ServerError, HTTP::TimeoutError => e
|
96
|
+
# FIXME: maybe only retry GET requests, for obvious reasons?
|
97
|
+
@failures += 1
|
98
|
+
raise e if @failures > @max_retries
|
99
|
+
warn "Redd got a #{e.class.name} error (#{e.message}), retrying..."
|
100
|
+
retry
|
101
|
+
else
|
102
|
+
@failures = 0
|
103
|
+
response
|
104
|
+
end
|
105
|
+
|
96
106
|
def connection
|
97
107
|
super.auth("Bearer #{@access.access_token}")
|
98
108
|
end
|
data/lib/redd/models/comment.rb
CHANGED
@@ -43,15 +43,17 @@ module Redd
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def default_loader
|
46
|
-
|
46
|
+
@attributes.key?(:link_id) ? load_with_comments : load_without_comments
|
47
|
+
end
|
48
|
+
|
49
|
+
def load_with_comments
|
47
50
|
id = @attributes.fetch(:id) { @attributes.fetch(:name).sub('t1_', '') }
|
51
|
+
link_id = @attributes[:link_id].sub('t3_', '')
|
52
|
+
@client.get("/comments/#{link_id}/_/#{id}").body[1][:data][:children][0][:data]
|
53
|
+
end
|
48
54
|
|
49
|
-
|
50
|
-
|
51
|
-
link_id = @attributes[:link_id].sub('t3_', '')
|
52
|
-
return @client.get("/comments/#{link_id}/_/#{id}").body[1][:data][:children][0][:data]
|
53
|
-
end
|
54
|
-
# We can only load the comment in isolation if we don't have the link_id.
|
55
|
+
def load_without_comments
|
56
|
+
id = @attributes.fetch(:id) { @attributes.fetch(:name).sub('t1_', '') }
|
55
57
|
@client.get('/api/info', id: "t1_#{id}").body[:data][:children][0][:data]
|
56
58
|
end
|
57
59
|
end
|
data/lib/redd/models/listing.rb
CHANGED
@@ -24,25 +24,21 @@ module Redd
|
|
24
24
|
# @param depth [Number] the maximum recursion depth
|
25
25
|
# @return [Array<Comment, MoreComments>] the expanded comments or self if past depth
|
26
26
|
def recursive_expand(link:, sort: nil, lookup: {}, depth: 10)
|
27
|
-
return [self] if depth
|
27
|
+
return [self] if depth <= 0
|
28
28
|
|
29
|
-
expand(link: link, sort: sort).
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
# If we can't find its parent (or if the parent is the submission), add it to the root.
|
34
|
-
next ary unless lookup.key?(thing.parent_id)
|
35
|
-
# Since the thing has a parent that we're tracking, attach it to the parent.
|
36
|
-
lookup[thing.parent_id].replies.children.concat(ary)
|
37
|
-
elsif thing.is_a?(Comment)
|
29
|
+
expand(link: link, sort: sort).each_with_object([]) do |thing, coll|
|
30
|
+
target = (lookup.key?(thing.parent_id) ? lookup[thing.parent_id].replies.children : coll)
|
31
|
+
|
32
|
+
if thing.is_a?(Comment)
|
38
33
|
# Add the comment to a lookup hash.
|
39
34
|
lookup[thing.name] = thing
|
40
35
|
# If the parent is not in the lookup hash, add it to the root listing.
|
41
|
-
|
42
|
-
|
43
|
-
|
36
|
+
target.push(thing)
|
37
|
+
elsif thing.is_a?(MoreComments) && thing.count > 0
|
38
|
+
# Get an array of expanded comments from the thing.
|
39
|
+
ary = thing.recursive_expand(link: link, sort: sort, lookup: lookup, depth: depth - 1)
|
40
|
+
target.concat(ary)
|
44
41
|
end
|
45
|
-
[]
|
46
42
|
end
|
47
43
|
end
|
48
44
|
|
@@ -9,7 +9,7 @@ module Redd
|
|
9
9
|
# @return [Comment, PrivateMessage] The created reply.
|
10
10
|
def reply(text)
|
11
11
|
fullname = get_attribute(:name)
|
12
|
-
@client.model(:post, '/api/comment
|
12
|
+
@client.model(:post, '/api/comment', text: text, thing_id: fullname).first
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
@@ -32,11 +32,16 @@ module Redd
|
|
32
32
|
end
|
33
33
|
|
34
34
|
# Set the sort order of the comments and reload if necessary.
|
35
|
-
# @param
|
36
|
-
def sort_order=(
|
37
|
-
|
38
|
-
|
39
|
-
|
35
|
+
# @param new_order [:confidence, :top, :controversial, :old, :qa] the sort order
|
36
|
+
def sort_order=(new_order)
|
37
|
+
# If the comments were loaded in a different sort order, delete them and invalidate this
|
38
|
+
# model.
|
39
|
+
if @attributes.key?(:comments) && @sort_order != new_order
|
40
|
+
@attributes.delete(:comments)
|
41
|
+
@definitely_fully_loaded = false
|
42
|
+
end
|
43
|
+
|
44
|
+
@sort_order = new_order
|
40
45
|
end
|
41
46
|
|
42
47
|
# Get all submissions for the same url.
|
@@ -105,6 +110,15 @@ module Redd
|
|
105
110
|
@client.post('/api/unlock', id: get_attribute(:name))
|
106
111
|
end
|
107
112
|
|
113
|
+
# Set the suggested sort order for comments for all users.
|
114
|
+
# @param suggested ['blank', 'confidence', 'top', 'new', 'controversial', 'old', 'random',
|
115
|
+
# 'qa', 'live'] the sort type
|
116
|
+
def set_suggested_sort(suggested) # rubocop:disable Style/AccessorMethodName
|
117
|
+
# Style/AccessorMethodName is disabled because it feels wrong for accessor methods to make
|
118
|
+
# HTTP requests.
|
119
|
+
@client.post('/api/set_suggested_sort', id: get_attribute(:name), sort: suggested)
|
120
|
+
end
|
121
|
+
|
108
122
|
private
|
109
123
|
|
110
124
|
def default_loader
|
@@ -234,6 +234,23 @@ module Redd
|
|
234
234
|
nil
|
235
235
|
end
|
236
236
|
|
237
|
+
# Remove the flair from a user
|
238
|
+
# @param thing [User, String] a User from which to remove flair
|
239
|
+
def delete_flair(user)
|
240
|
+
name = user.is_a?(User) ? user.name : user
|
241
|
+
@client.post("/r/#{get_attribute(:display_name)}/api/deleteflair", name: name)
|
242
|
+
end
|
243
|
+
|
244
|
+
# Set a Submission's or User's flair based on a flair template id.
|
245
|
+
# @param thing [User, Submission] an object to assign a template to
|
246
|
+
# @param template_id [String] the UUID of the flair template to assign
|
247
|
+
# @param text [String] optional text for the flair
|
248
|
+
def set_flair_template(thing, template_id, text: nil)
|
249
|
+
key = thing.is_a?(User) ? :name : :link
|
250
|
+
params = { key => thing.name, flair_template_id: template_id, text: text }
|
251
|
+
@client.post("/r/#{get_attribute(:display_name)}/api/selectflair", params)
|
252
|
+
end
|
253
|
+
|
237
254
|
# Add the subreddit to the user's subscribed subreddits.
|
238
255
|
def subscribe(action: :sub, skip_initial_defaults: false)
|
239
256
|
@client.post(
|
@@ -6,16 +6,30 @@ module Redd
|
|
6
6
|
module Models
|
7
7
|
# A reddit user.
|
8
8
|
class WikiPage < LazyModel
|
9
|
+
# Edit the wiki page.
|
10
|
+
# @param content [String] the new wiki page contents
|
11
|
+
# @param reason [String, nil] an optional reason for editing the page
|
12
|
+
def edit(content, reason: nil)
|
13
|
+
params = { page: @attributes.fetch(:title), content: content }
|
14
|
+
params[:reason] = reason if reason
|
15
|
+
@client.post("/r/#{@attributes.fetch(:subreddit).display_name}/api/wiki/edit", params)
|
16
|
+
end
|
17
|
+
|
9
18
|
private
|
10
19
|
|
11
20
|
def default_loader
|
12
21
|
title = @attributes.fetch(:title)
|
13
22
|
if @attributes.key?(:subreddit)
|
14
|
-
sr_name = attributes[:subreddit].display_name
|
23
|
+
sr_name = @attributes[:subreddit].display_name
|
15
24
|
return @client.get("/r/#{sr_name}/wiki/#{title}").body[:data]
|
16
25
|
end
|
17
26
|
@client.get("/wiki/#{title}").body[:data]
|
18
27
|
end
|
28
|
+
|
29
|
+
def after_initialize
|
30
|
+
return unless @attributes[:revision_by]
|
31
|
+
@attributes[:revision_by] = @client.unmarshal(@attributes[:revision_by])
|
32
|
+
end
|
19
33
|
end
|
20
34
|
end
|
21
35
|
end
|
@@ -6,9 +6,13 @@ module Redd
|
|
6
6
|
module Utilities
|
7
7
|
# Handles response errors in API responses.
|
8
8
|
class ErrorHandler
|
9
|
+
AUTH_HEADER = 'www-authenticate'
|
10
|
+
INVALID_TOKEN = 'invalid_token'
|
11
|
+
INSUFFICIENT_SCOPE = 'insufficient_scope'
|
12
|
+
|
9
13
|
HTTP_ERRORS = {
|
10
14
|
400 => Redd::BadRequest,
|
11
|
-
|
15
|
+
403 => Redd::Forbidden,
|
12
16
|
404 => Redd::NotFound,
|
13
17
|
500 => Redd::ServerError,
|
14
18
|
502 => Redd::ServerError,
|
@@ -16,26 +20,43 @@ module Redd
|
|
16
20
|
504 => Redd::ServerError
|
17
21
|
}.freeze
|
18
22
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
+
def check_error(res, raw:)
|
24
|
+
# Check for status code-based errors first and return it if we found one.
|
25
|
+
error = invalid_access_error(res) || insufficient_scope_error(res) || other_http_error(res)
|
26
|
+
return error if error || raw
|
23
27
|
|
24
|
-
|
28
|
+
# If there wasn't an status code error and we're allowed to look into the response, parse
|
29
|
+
# it and check for errors.
|
25
30
|
# TODO: deal with errors of type { fields:, explanation:, message:, reason: }
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
31
|
+
api_error(res)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Deal with an error caused by having an expired or invalid access token.
|
37
|
+
def invalid_access_error(res)
|
38
|
+
return nil unless res.code == 401 && res.headers[AUTH_HEADER] &&
|
39
|
+
res.headers[AUTH_HEADER].include?(INVALID_TOKEN)
|
40
|
+
InvalidAccess.new(res)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Deal with an error caused by not having enough the correct scope
|
44
|
+
def insufficient_scope_error(res)
|
45
|
+
return nil unless res.code == 403 && res.headers[AUTH_HEADER] &&
|
46
|
+
res.headers[AUTH_HEADER].include?(INSUFFICIENT_SCOPE)
|
47
|
+
InsufficientScope.new(res)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Deal with an error signalled by the HTTP response code.
|
51
|
+
def other_http_error(res)
|
52
|
+
HTTP_ERRORS[res.code].new(res) if HTTP_ERRORS.key?(res.code)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Deal with those annoying errors that come with perfect 200 status codes.
|
56
|
+
def api_error(res)
|
57
|
+
return nil unless res.is_a?(Hash) && res.body[:json] && res.body[:json][:errors] &&
|
58
|
+
!res.body[:json][:errors].empty?
|
59
|
+
APIError.new(res)
|
39
60
|
end
|
40
61
|
end
|
41
62
|
end
|
@@ -14,7 +14,6 @@ module Redd
|
|
14
14
|
't5' => Models::Subreddit,
|
15
15
|
'more' => Models::MoreComments,
|
16
16
|
'wikipage' => Models::WikiPage,
|
17
|
-
'Listing' => Models::Listing,
|
18
17
|
'modaction' => Models::Subreddit::ModAction,
|
19
18
|
'LabeledMulti' => Models::Multireddit,
|
20
19
|
'LiveUpdate' => Models::LiveThread::LiveUpdate
|
@@ -24,18 +23,41 @@ module Redd
|
|
24
23
|
@client = client
|
25
24
|
end
|
26
25
|
|
27
|
-
def unmarshal(
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
26
|
+
def unmarshal(res)
|
27
|
+
# I'm loving the hell out of this pattern.
|
28
|
+
model = js_listing(res) || js_model(res) || api_listing(res) || api_model(res)
|
29
|
+
raise "cannot unmarshal: #{res.inspect}" if model.nil?
|
30
|
+
model
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# Unmarshal frontent API-style listings
|
36
|
+
def js_listing(res)
|
37
|
+
# One day I'll get to deprecate Ruby 2.2 and jump into the world of Hash#dig.
|
38
|
+
return nil unless res[:json] && res[:json][:data] && res[:json][:data][:things]
|
39
|
+
Models::Listing.new(@client, children: res[:json][:data][:things].map { |t| unmarshal(t) })
|
40
|
+
end
|
41
|
+
|
42
|
+
# Unmarshal frontend API-style models.
|
43
|
+
def js_model(res)
|
44
|
+
# FIXME: deprecate this? this shouldn't be happening in the API, so this is better handled
|
45
|
+
# in the respective classes.
|
46
|
+
Models::BasicModel.new(@client, res[:json][:data]) if res[:json] && res[:json][:data]
|
47
|
+
end
|
48
|
+
|
49
|
+
# Unmarshal API-provided listings.
|
50
|
+
def api_listing(res)
|
51
|
+
return nil unless res[:kind] == 'Listing'
|
52
|
+
attributes = res[:data]
|
53
|
+
attributes[:children].map! { |child| unmarshal(child) }
|
54
|
+
Models::Listing.new(@client, attributes)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Unmarshal API-provided model.
|
58
|
+
def api_model(res)
|
59
|
+
return nil unless MAPPING[res[:kind]]
|
60
|
+
MAPPING[res[:kind]].new(@client, res[:data])
|
39
61
|
end
|
40
62
|
end
|
41
63
|
end
|
data/lib/redd/version.rb
CHANGED
data/redd.gemspec
CHANGED
@@ -22,9 +22,9 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
23
|
spec.require_paths = ['lib']
|
24
24
|
|
25
|
-
spec.add_dependency 'http', '~> 2.
|
25
|
+
spec.add_dependency 'http', '~> 2.2'
|
26
26
|
|
27
|
-
spec.add_development_dependency 'bundler', '~> 1.
|
27
|
+
spec.add_development_dependency 'bundler', '~> 1.14'
|
28
28
|
spec.add_development_dependency 'rake', '~> 12.0'
|
29
29
|
spec.add_development_dependency 'rubocop', '~> 0.47'
|
30
30
|
spec.add_development_dependency 'pry', '~> 0.10'
|
@@ -32,4 +32,5 @@ Gem::Specification.new do |spec|
|
|
32
32
|
spec.add_development_dependency 'rspec', '~> 3.5'
|
33
33
|
spec.add_development_dependency 'simplecov', '~> 0.13'
|
34
34
|
spec.add_development_dependency 'webmock', '~> 2.3'
|
35
|
+
spec.add_development_dependency 'vcr', '~> 3.0'
|
35
36
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redd
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Avinash Dwarapu
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-03-
|
11
|
+
date: 2017-03-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: http
|
@@ -16,28 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '2.
|
19
|
+
version: '2.2'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '2.
|
26
|
+
version: '2.2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '1.
|
33
|
+
version: '1.14'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '1.
|
40
|
+
version: '1.14'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rake
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -122,6 +122,20 @@ dependencies:
|
|
122
122
|
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '2.3'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: vcr
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '3.0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '3.0'
|
125
139
|
description:
|
126
140
|
email:
|
127
141
|
- avinash@dwarapu.me
|
@@ -130,6 +144,7 @@ extensions: []
|
|
130
144
|
extra_rdoc_files: []
|
131
145
|
files:
|
132
146
|
- ".gitignore"
|
147
|
+
- ".hound.yml"
|
133
148
|
- ".rspec"
|
134
149
|
- ".rubocop.yml"
|
135
150
|
- ".travis.yml"
|