redd 0.8.4 → 0.8.5
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/.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"
|