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 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"