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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ce5057c5421178a5b9ad6a056b2fa33a161413f8
4
- data.tar.gz: 11c8973bcb1fbd2786c40e3f929229c944883da0
3
+ metadata.gz: c41f2214b01198dd4f0a44f243bc73364af33b6c
4
+ data.tar.gz: a33587b7a36a41c11b3a4faa51814af24d86565f
5
5
  SHA512:
6
- metadata.gz: 712701e751a6cc9e5bb552fafc451c22c3cfefd275da19305620e57b306f5645ae087484217f3049256424d3d40c55d786dbeef4f123c358845d24c94b7cec84
7
- data.tar.gz: ac462c530eda68e27718333a386625f6b0921a99a0676a49655721b23d6ccddadf75b433a8d329d0e05f19217565d5029f607af6703b580d374323f1b1764d0d
6
+ metadata.gz: ba2ecdabcf4c991710428b08e57437a00e0b5c85a0164d7786fe4363f65f51617e544519393bc48cae3672debaa0b8c2ea4743aae67ae1e23dedfee32e24dfe9
7
+ data.tar.gz: ef71ea37b3375b7328f9f6f6298d8cb867dda0aa38eec6b0480fcc12063b935a6fcfea99216d0bac5563577fd8a86c98697ef1a697ebfab09365a3e60390e3a5
data/.gitignore CHANGED
@@ -6,4 +6,5 @@
6
6
  /doc/
7
7
  /pkg/
8
8
  /spec/reports/
9
+ /spec/examples.txt
9
10
  /tmp/
data/.hound.yml ADDED
@@ -0,0 +1,2 @@
1
+ ruby:
2
+ config_file: .rubocop.yml
data/.rspec CHANGED
@@ -1,3 +1,3 @@
1
- --format documentation
1
+ --format progress
2
2
  --require spec_helper
3
3
  --color
data/.travis.yml CHANGED
@@ -11,5 +11,5 @@ matrix:
11
11
  - rvm: ruby-head
12
12
  - rvm: jruby-head
13
13
 
14
- before_install: gem install bundler -v 1.13.7
14
+ before_install: gem install bundler -v 1.14.6
15
15
  cache: bundler
@@ -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
- response = @rate_limiter.after_limit { super(verb, path, params: api_params, **options) }
72
- # Check for errors in the returned response
73
- response_error = @error_handler.check_error(response, raw: raw)
74
- raise response_error unless response_error.nil?
75
- # All done, return the response
76
- @failures = 0
77
- response
78
- rescue Redd::ServerError, HTTP::TimeoutError => e
79
- # FIXME: maybe only retry GET requests, for obvious reasons?
80
- @failures += 1
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
@@ -43,15 +43,17 @@ module Redd
43
43
  end
44
44
 
45
45
  def default_loader
46
- # Ensure we have the comment's id.
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
- # If we have the link_id, we can load the listing with replies.
50
- if @attributes.key?(:link_id)
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
@@ -19,12 +19,6 @@ module Redd
19
19
  get_attribute(:children).public_send(method_name, *args, &block)
20
20
  end
21
21
  end
22
-
23
- private
24
-
25
- def after_initialize
26
- @attributes.fetch(:children).map! { |el| @client.unmarshal(el) }
27
- end
28
22
  end
29
23
  end
30
24
  end
@@ -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 == 0
27
+ return [self] if depth <= 0
28
28
 
29
- expand(link: link, sort: sort).flat_map do |thing|
30
- if thing.is_a?(MoreComments) && thing.count > 0
31
- # Get an array of expanded comments from the thing.
32
- ary = thing.recursive_expand(link: link, sort: sort, lookup: lookup, depth: depth - 1)
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
- next thing unless lookup.key?(thing.parent_id)
42
- # If the parent was found, add the child to the parent's replies instead.
43
- lookup[thing.parent_id].replies.children << thing
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/', text: text, thing_id: fullname).first
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 order [:confidence, :top, :controversial, :old, :qa] the sort order
36
- def sort_order=(order)
37
- @sort_order = order
38
- reload if @definitely_fully_loaded
39
- order
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
- # 403 => Redd::Forbidden,
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
- AUTHORIZATION_ERRORS = {
20
- 'insufficient_scope' => Redd::InsufficientScope,
21
- 'invalid_token' => Redd::InvalidAccess
22
- }.freeze
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
- def check_error(response, raw:)
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
- if !raw && response.body.is_a?(Hash) && response.body[:json] &&
27
- response.body[:json][:errors] && !response.body[:json][:errors].empty?
28
- Redd::APIError.new(response)
29
- elsif HTTP_ERRORS.key?(response.code)
30
- HTTP_ERRORS[response.code].new(response)
31
- elsif response.code == 401 || response.code == 403
32
- # FIXME: i think insufficient_scope comes with 403 and invalid_token with 401
33
- AUTHORIZATION_ERRORS.each do |key, klass|
34
- auth_header = response.headers['www-authenticate']
35
- return klass.new(response) if auth_header && auth_header.include?(key)
36
- end
37
- nil
38
- end
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
@@ -4,8 +4,8 @@ module Redd
4
4
  module Utilities
5
5
  # Manages rate limiting by sleeping.
6
6
  class RateLimiter
7
- def initialize(gap = 1)
8
- @gap = 1
7
+ def initialize(gap)
8
+ @gap = gap
9
9
  @last_request_time = Time.now - gap
10
10
  end
11
11
 
@@ -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(response)
28
- if response[:json] && response[:json][:data]
29
- if response[:json][:data][:things]
30
- Models::Listing.new(@client, children: response[:json][:data][:things])
31
- else
32
- Models::BasicModel.new(@client, response[:json][:data])
33
- end
34
- elsif MAPPING.key?(response[:kind])
35
- MAPPING[response[:kind]].new(@client, response[:data])
36
- else
37
- raise "unknown type to unmarshal: #{response[:kind].inspect}"
38
- end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Redd
4
- VERSION = '0.8.4'
4
+ VERSION = '0.8.5'
5
5
  end
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.1'
25
+ spec.add_dependency 'http', '~> 2.2'
26
26
 
27
- spec.add_development_dependency 'bundler', '~> 1.13'
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
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-10 00:00:00.000000000 Z
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.1'
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.1'
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.13'
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.13'
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"