redd 0.8.0.pre.1 → 0.8.0.pre.2

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: bc08060b2e7111adfd18a83c7d23e464f20a92dc
4
- data.tar.gz: 79897c294f7f612e90acee7184b91a4982d54cb6
3
+ metadata.gz: 69d6cf992f71356b1db86a000bdc3be4508e351c
4
+ data.tar.gz: 01d0b098fb7c26a91e34ff001310417c93e3bd74
5
5
  SHA512:
6
- metadata.gz: 0ccfa94a4923e3748931c6ca5e5515f23c10bd23d979c1ef79e5f3f62a87c78ff66ed01f72033d95d3da3e8ca0e47cf9ebd412d758d3b751d3d935545abd787c
7
- data.tar.gz: f2881316388b4b3b1183aebdc867184aa64750c57ac29cfb6b17532ef099f0c4de309f064f3160a3eb670ee4d70d91cee335367b9c43a34c5f70eb3254781743
6
+ metadata.gz: 4019ce12ee1cfac8927642bfdd327e4221835842a230ad73bce35b0ce3e1f0445523bf5a796aeca182b63c7061861ba1a28a587fe8814594d6d89531ece2c3a6
7
+ data.tar.gz: 292775ce9121b4de029ee49e451417508b64524940043569ebe53b807387fac0b1539a7cf5e37a13d494c83267421a1789a75e00ec96cadbaa2cf01128c0f9b7
data/.rubocop.yml CHANGED
@@ -4,6 +4,10 @@ AllCops:
4
4
  Metrics/LineLength:
5
5
  Max: 100
6
6
 
7
+ # Dumb, but also brand new.
8
+ Style/NumericPredicate:
9
+ Enabled: false
10
+
7
11
  # Allows for raising Redd::ResponseErrors with a Response object.
8
12
  Style/RaiseArgs:
9
13
  Enabled: false
data/TODO.md CHANGED
@@ -1,11 +1,7 @@
1
1
  # v0.8.0 checklist
2
2
 
3
- - [ ] Make v0.8.0 feature-complete with v0.7.x
4
- - [ ] `Messageable`
5
- - [ ] `MoreComments` and expansion
6
3
  - [ ] make rubocop fail on error
7
4
  - [ ] unduplicate duplicated code
8
5
  - [ ] Write tests (oh boy)
9
- - [ ] More comprehensive error checking
10
- - [ ] A `FrontPage` object
6
+ - [ ] abstract out listing methods?
11
7
  - [ ] Add `#==` and `#to_s` methods to models
data/bin/console CHANGED
@@ -37,7 +37,7 @@ server.mount_proc '/authenticate' do |_, res|
37
37
  state: '0',
38
38
  redirect_uri: 'http://localhost:8000/redirect',
39
39
  'scope': %w(identity read subscribe privatemessages wikiread submit vote edit modposts history
40
- modflair)
40
+ modflair modconfig)
41
41
  )
42
42
  )
43
43
  end
@@ -64,37 +64,21 @@ module Redd
64
64
  @unmarshaller.unmarshal(object)
65
65
  end
66
66
 
67
- def model(verb, path, params = {})
67
+ def model(verb, path, options = {})
68
68
  # XXX: make unmarshal explicit in methods?
69
- unmarshal(send(verb, path, params).body)
70
- end
71
-
72
- private
73
-
74
- # Makes sure a valid access is present, raising an error if nil
75
- def ensure_access_is_valid
76
- # Authenticate first if auto_login is enabled
77
- authenticate if @access.nil? && @auto_login
78
- # Refresh access if auto_refresh is enabled
79
- refresh if @access.expired? && @auto_refresh
80
- # Fuck it, panic
81
- raise 'client access is nil, try calling #authenticate' if @access.nil?
82
- end
83
-
84
- def connection
85
- super.auth("Bearer #{@access.access_token}")
69
+ unmarshal(request(verb, path, options).body)
86
70
  end
87
71
 
88
72
  # Makes a request, ensuring not to break the rate limit by sleeping.
89
73
  # @see Client#request
90
- def request(verb, path, params: {}, form: {})
74
+ def request(verb, path, raw: false, params: {}, **options)
91
75
  # Make sure @access is populated by a valid access
92
76
  ensure_access_is_valid
93
77
  # Setup base API params and make request
94
78
  api_params = { api_type: 'json', raw_json: 1 }.merge(params)
95
- response = @rate_limiter.after_limit { super(verb, path, params: api_params, form: form) }
79
+ response = @rate_limiter.after_limit { super(verb, path, params: api_params, **options) }
96
80
  # Check for errors in the returned response
97
- response_error = @error_handler.check_error(response)
81
+ response_error = @error_handler.check_error(response, raw: raw)
98
82
  raise response_error unless response_error.nil?
99
83
  # All done, return the response
100
84
  @failures = 0
@@ -105,5 +89,21 @@ module Redd
105
89
  warn "Redd got a #{e.class.name} error (#{e.message}), retrying..."
106
90
  retry
107
91
  end
92
+
93
+ private
94
+
95
+ # Makes sure a valid access is present, raising an error if nil
96
+ def ensure_access_is_valid
97
+ # Authenticate first if auto_login is enabled
98
+ authenticate if @access.nil? && @auto_login
99
+ # Refresh access if auto_refresh is enabled
100
+ refresh if @access.expired? && @auto_refresh
101
+ # Fuck it, panic
102
+ raise 'client access is nil, try calling #authenticate' if @access.nil?
103
+ end
104
+
105
+ def connection
106
+ super.auth("Bearer #{@access.access_token}")
107
+ end
108
108
  end
109
109
  end
data/lib/redd/client.rb CHANGED
@@ -24,6 +24,19 @@ module Redd
24
24
  @user_agent = user_agent
25
25
  end
26
26
 
27
+ # Make an HTTP request.
28
+ # @param verb [:get, :post, :put, :patch, :delete] the HTTP verb to use
29
+ # @param path [String] the path relative to the endpoint
30
+ # @param options [Hash] the request parameters
31
+ # @option options [Hash] :params the parameters to supply with the url
32
+ # @option options [Hash] :form the parameters to supply in the body
33
+ # @option options [Hash] :body the direct body contents
34
+ # @return [Response] the response
35
+ def request(verb, path, options = {})
36
+ response = connection.request(verb, path, **options)
37
+ Response.new(response.status.code, response.headers, response.body.to_s)
38
+ end
39
+
27
40
  # Make a GET request.
28
41
  # @param path [String] the path relative to the endpoint
29
42
  # @param options [Hash] the parameters to supply
@@ -73,16 +86,5 @@ module Redd
73
86
  .headers('User-Agent' => @user_agent)
74
87
  .timeout(:per_operation, write: 5, connect: 5, read: 5)
75
88
  end
76
-
77
- # Make an HTTP request.
78
- # @param verb [:get, :post, :put, :patch, :delete] the HTTP verb to use
79
- # @param path [String] the path relative to the endpoint
80
- # @param params [Hash] the parameters to supply with the url
81
- # @param form [Hash] the parameters to supply in the body
82
- # @return [Response] the response
83
- def request(verb, path, params: {}, form: {})
84
- response = connection.request(verb, path, params: params, form: form)
85
- Response.new(response.status.code, response.headers, response.body.to_s)
86
- end
87
89
  end
88
90
  end
data/lib/redd/error.rb CHANGED
@@ -1,6 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Redd
4
+ # An error with the API.
5
+ class APIError < StandardError
6
+ attr_reader :response, :name
7
+
8
+ def initialize(response)
9
+ @response = response
10
+ @name, message = response.body[:json][:errors][0]
11
+ super(message)
12
+ end
13
+ end
14
+
4
15
  # Represents an error from reddit returned in a response.
5
16
  class ResponseError < StandardError
6
17
  attr_accessor :response
@@ -14,6 +25,9 @@ module Redd
14
25
  # An error with Redd, probably (let me know!)
15
26
  class BadRequest < ResponseError; end
16
27
 
28
+ # Whatever it is, you're not allowed to do it.
29
+ class Forbidden < ResponseError; end
30
+
17
31
  # You don't have the correct scope to do this.
18
32
  class InsufficientScope < ResponseError; end
19
33
 
@@ -61,15 +61,15 @@ module Redd
61
61
  # @param include_private [Boolean] whether to also include private methods
62
62
  # @return [Boolean] whether the method is handled by method_missing
63
63
  def respond_to_missing?(method_name, include_private = false)
64
- attribute?(method_name) || attribute?(depredicate(method_name)) || super
64
+ @attributes.key?(method_name) || @attributes.key?(depredicate(method_name)) || super
65
65
  end
66
66
 
67
67
  # Return an attribute or raise a NoMethodError if it doesn't exist.
68
68
  # @param method_name [Symbol] the name of the attribute
69
69
  # @return [Object] the result of the attribute check
70
70
  def method_missing(method_name, *args, &block)
71
- return get_attribute(method_name) if attribute?(method_name)
72
- return get_attribute(depredicate(method_name)) if attribute?(depredicate(method_name))
71
+ return get_attribute(method_name) if @attributes.key?(method_name)
72
+ return get_attribute(depredicate(method_name)) if @attributes.key?(depredicate(method_name))
73
73
  super
74
74
  end
75
75
 
@@ -113,12 +113,6 @@ module Redd
113
113
  # Fetch the attribute, raising a KeyError if it doesn't exist.
114
114
  @attributes.fetch(name)
115
115
  end
116
-
117
- # @param name [Symbol] the name of the attribute to check
118
- # @return [Boolean] whether the attribute exists
119
- def attribute?(name)
120
- @attributes.key?(name)
121
- end
122
116
  end
123
117
  end
124
118
  end
@@ -28,8 +28,10 @@ module Redd
28
28
  # @option hash [String] :id the comment's id (e.g. abc123)
29
29
  # @return [Comment]
30
30
  def self.from_response(client, hash)
31
+ # FIXME: listings can be empty... (for some reason)
32
+
31
33
  # Ensure we have the comment's id.
32
- id = hash.fetch(:id, hash.fetch(:name).tr('t1_', ''))
34
+ id = hash.fetch(:id) { hash.fetch(:name).tr('t1_', '') }
33
35
 
34
36
  # If we have the link_id, we can load the listing with replies.
35
37
  if hash.key?(:link_id)
@@ -46,6 +48,12 @@ module Redd
46
48
  c.get('/api/info', id: "t1_#{id}").body[:data][:children][0][:data]
47
49
  end
48
50
  end
51
+
52
+ private
53
+
54
+ def after_initialize
55
+ @attributes[:replies] = [] if !@attributes.key?(:replies) || @attributes[:replies] == ''
56
+ end
49
57
  end
50
58
  end
51
59
  end
@@ -40,6 +40,23 @@ module Redd
40
40
  super
41
41
  end
42
42
 
43
+ # Checks whether an attribute is supported by method_missing. Since we don't know whether an
44
+ # attribute exists until we load it, we have to respond true until we load it.
45
+ # @param method_name [Symbol] the method name or attribute to check
46
+ # @param include_private [Boolean] whether to also include private methods
47
+ # @return [Boolean] whether the method is handled by method_missing
48
+ def respond_to_missing?(method_name, include_private = false)
49
+ @definitely_fully_loaded ? super : true
50
+ end
51
+
52
+ # Return an attribute or raise a NoMethodError if it doesn't exist.
53
+ # @param method_name [Symbol] the name of the attribute
54
+ # @return [Object] the result of the attribute check
55
+ def method_missing(method_name, *args, &block)
56
+ ensure_fully_loaded unless @attributes.key?(method_name)
57
+ super
58
+ end
59
+
43
60
  private
44
61
 
45
62
  # Make sure the model is loaded at least once.
@@ -49,12 +66,7 @@ module Redd
49
66
 
50
67
  # Gets the attribute and loads it if it may be available from the response.
51
68
  def get_attribute(name)
52
- ensure_fully_loaded unless @attributes.key?(name)
53
- super
54
- end
55
-
56
- # Checks whether an attribute exists, loading it first if necessary.
57
- def attribute?(name)
69
+ # XXX: Replace get_attribute calls with simple method calls?
58
70
  ensure_fully_loaded unless @attributes.key?(name)
59
71
  super
60
72
  end
@@ -16,7 +16,7 @@ module Redd
16
16
  new(client, hash)
17
17
  end
18
18
 
19
- %i([] each empty? length size).each do |method_name|
19
+ %i([] each empty? first last).each do |method_name|
20
20
  define_method(method_name) do |*args, &block|
21
21
  get_attribute(:children).public_send(method_name, *args, &block)
22
22
  end
@@ -1,10 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'lazy_model'
3
+ require_relative 'basic_model'
4
4
 
5
5
  module Redd
6
6
  module Models
7
- class MoreComments < LazyModel
7
+ # An object that represents a bunch of comments that need to be expanded.
8
+ class MoreComments < BasicModel
9
+ # Expand the object's children into a listing of Comments and MoreComments.
10
+ # @param link [String] the fullname of the submission the object belongs to
11
+ # @param sort [String] the sort order of the submission
12
+ # @return [Listing<Comment, MoreComments>] the expanded children
13
+ def expand(link:, sort: 'best')
14
+ @client.model(
15
+ :get, '/api/morechildren',
16
+ link_id: link,
17
+ children: get_attribute(:children).join(','),
18
+ sort: sort
19
+ )
20
+ end
21
+
22
+ # Keep expanding until all top-level MoreComments are converted to comments
23
+ def recursive_expand(link:, sort: 'best')
24
+ # FIXME: this returns a flattened listing of comments and doesn't preserve the structure
25
+ expand(link: link, sort: sort).flat_map do |o|
26
+ o.is_a?(MoreComments) && o.count > 0 ? o.recursive_expand(link: link, sort: sort) : [o]
27
+ end
28
+ end
8
29
  end
9
30
  end
10
31
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Redd
4
+ module Models
5
+ # Applied to {Session} for site-wide and {Subreddit} for subreddit-specific search.
6
+ module Searchable
7
+ # Search reddit.
8
+ # @see https://www.reddit.com/wiki/search
9
+ #
10
+ # @param query [String] the search query
11
+ # @param params [Hash] the search params
12
+ # @option params [:cloudsearch, :lucene, :plain] :syntax the query's syntax
13
+ # @option params [String] :after return results after the given fullname
14
+ # @option params [String] :before return results before the given fullname
15
+ # @option params [Integer] :count the number of items already seen in the listing
16
+ # @option params [1..100] :limit the maximum number of things to return
17
+ # @option params [:hour, :day, :week, :month, :year, :all] :time the time period to restrict
18
+ # search results by
19
+ # @option params [:relevance, :hot, :top, :new, :comments] :sort the sort order of results
20
+ # @option params [String] :restrict_to restrict by subreddit (prefer {Subreddit#search})
21
+ # @return [Listing<Comment, Submission>] the search results
22
+ def search(query, **params)
23
+ params[:q] = query if query
24
+ params[:t] = params.delete(:time) if params.key?(:time)
25
+ if params[:restrict_to]
26
+ subreddit = params.delete(:restrict_to)
27
+ params[:restrict_sr] = true
28
+ @client.model(:get, "/r/#{subreddit}/search", params)
29
+ else
30
+ @client.model(:get, '/search', params)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'lazy_model'
4
+ require_relative 'searchable'
4
5
 
5
6
  module Redd
6
7
  module Models
7
8
  # The starter class.
8
9
  class Session < BasicModel
10
+ include Searchable
11
+
9
12
  # @return [FrontPage] the user's front page
10
13
  def front_page
11
14
  FrontPage.new(@client)
@@ -45,7 +48,7 @@ module Redd
45
48
  # Get submissions or comments by their fullnames.
46
49
  # @param fullnames [String, Array<String>] one or an array of fullnames (e.g. t3_abc1234)
47
50
  # @return [Listing<Submission, Comment>]
48
- def get(fullnames)
51
+ def from_ids(fullnames)
49
52
  # XXX: Could we use better methods for t1_ and t3_?
50
53
  @client.model(:get, '/api/info', id: Array(fullnames).join(','))
51
54
  end
@@ -25,16 +25,68 @@ module Redd
25
25
  def self.from_response(client, hash)
26
26
  link_id = hash.fetch(:id)
27
27
  new(client, hash) do |c|
28
- # `details` is a pair (2-element array):
29
- # - details[0] is a one-item listing containing the submission
30
- # - details[1] is listing of comments
31
- details = c.get("/comments/#{link_id}").body
32
- comments = details[1][:data][:children].map do |comment_object|
33
- Comment.from_response(c, comment_object[:data])
34
- end
35
- details[0][:data][:children][0][:data].merge(comments: comments)
28
+ # `data` is a pair (2-element array):
29
+ # - data[0] is a one-item listing containing the submission
30
+ # - data[1] is listing of comments
31
+ data = c.get("/comments/#{link_id}").body
32
+ data[0][:data][:children][0][:data].merge(comments: c.unmarshal(data[1]))
36
33
  end
37
34
  end
35
+
36
+ # Mark the link as "Not Suitable For Work".
37
+ def mark_as_nsfw
38
+ @client.get('/api/marknsfw', id: get_attribute(:name))
39
+ @attributes[:over_18] = true
40
+ end
41
+
42
+ # No longer mark the link as "Not Suitable For Work".
43
+ def unmark_as_nsfw
44
+ @client.get('/api/unmarknsfw', id: get_attribute(:name))
45
+ @attributes[:over_18] = false
46
+ end
47
+
48
+ # Mark the link as a spoiler.
49
+ def mark_as_spoiler
50
+ @client.get('/api/spoiler', id: get_attribute(:name))
51
+ @attributes[:spoiler] = true
52
+ end
53
+
54
+ # No longer mark the link as a spoiler.
55
+ def unmark_as_spoiler
56
+ @client.get('/api/unspoiler', id: get_attribute(:name))
57
+ @attributes[:spoiler] = false
58
+ end
59
+
60
+ # Set the submission to "contest mode" (comments are randomly sorted)
61
+ def enable_contest_mode
62
+ @client.post('/api/set_contest_mode', id: get_attribute(:name), state: true)
63
+ end
64
+
65
+ # Disable the "contest mode".
66
+ def disable_contest_mode
67
+ @client.post('/api/set_contest_mode', id: get_attribute(:name), state: false)
68
+ end
69
+
70
+ # Set the submission as the sticky post of the subreddit.
71
+ # @param slot [1, 2] which "slot" to place the sticky on
72
+ def make_sticky(slot: nil)
73
+ @client.post('/api/set_subreddit_sticky', id: get_attribute(:name), num: slot, state: true)
74
+ end
75
+
76
+ # Unsticky the post from the subreddit.
77
+ def remove_sticky
78
+ @client.post('/api/set_subreddit_sticky', id: get_attribute(:name), state: false)
79
+ end
80
+
81
+ # Prevent users from commenting on the link (and hide it as well).
82
+ def lock
83
+ @client.post('/api/lock', id: get_attribute(:name))
84
+ end
85
+
86
+ # Allow users to comment on the link again.
87
+ def unlock
88
+ @client.post('/api/unlock', id: get_attribute(:name))
89
+ end
38
90
  end
39
91
  end
40
92
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'lazy_model'
4
4
  require_relative 'messageable'
5
+ require_relative 'searchable'
5
6
  require_relative '../utilities/stream'
6
7
 
7
8
  module Redd
@@ -9,6 +10,24 @@ module Redd
9
10
  # A subreddit.
10
11
  class Subreddit < LazyModel
11
12
  include Messageable
13
+ include Searchable
14
+
15
+ # A list of keys that are required by #modify_settings.
16
+ SETTINGS_KEYS = %i(allow_images allow_top collapse_deleted_comments comment_score_hide_mins
17
+ description exclude_banned_modqueue header-title hide_ads lang link_type
18
+ over_18 public_description public_traffic show_media show_media_preview
19
+ spam_comments spam_links spam_selfposts spoilers_enabled submit_link_label
20
+ submit_text submit_text_label suggested_comment_sort theme_sr
21
+ theme_sr_update title type wiki_edit_age wiki_edit_karma wikimode).freeze
22
+
23
+ # A mapping from keys returned by #settings to keys required by #modify_settings
24
+ SETTINGS_MAP = {
25
+ subreddit_type: :type,
26
+ language: :lang,
27
+ content_options: :link_type,
28
+ default_set: :allow_top,
29
+ header_hover_text: :'header-title'
30
+ }.freeze
12
31
 
13
32
  # Make a Subreddit from its name.
14
33
  # @option hash [String] :display_name the subreddit's name
@@ -38,6 +57,15 @@ module Redd
38
57
  WikiPage.from_response(@client, title: title, subreddit: self)
39
58
  end
40
59
 
60
+ # Search a subreddit.
61
+ # @param query [String] the search query
62
+ # @param params [Hash] refer to {Searchable} to see search parameters
63
+ # @see Searchable#search
64
+ def search(query, **params)
65
+ restricted_params = { restrict_to: get_attribute(:display_name) }.merge(params)
66
+ super(query, restricted_params)
67
+ end
68
+
41
69
  # @!group Listings
42
70
 
43
71
  # Get the appropriate listing.
@@ -54,7 +82,7 @@ module Redd
54
82
  # @return [Listing<Submission, Comment>]
55
83
  def listing(sort, **params)
56
84
  params[:t] = params.delete(:time) if params.key?(:time)
57
- @client.model(:get, "/r/#{get_attribute(:display_name)}/#{sort}.json", params)
85
+ @client.model(:get, "/r/#{get_attribute(:display_name)}/#{sort}", params)
58
86
  end
59
87
 
60
88
  # @!method hot(**params)
@@ -83,7 +111,7 @@ module Redd
83
111
  #
84
112
  # @return [Listing<Submission, Comment>]
85
113
  def moderator_listing(type, **params)
86
- @client.model(:get, "/r/#{get_attribute(:display_name)}/about/#{type}.json", params)
114
+ @client.model(:get, "/r/#{get_attribute(:display_name)}/about/#{type}", params)
87
115
  end
88
116
 
89
117
  # @!method reports(**params)
@@ -97,6 +125,38 @@ module Redd
97
125
  define_method(type) { |**params| moderator_listing(type, **params) }
98
126
  end
99
127
 
128
+ # @!endgroup
129
+ # @!group Relationship Listings
130
+
131
+ # Get the appropriate relationship listing.
132
+ # @param type [:banned, :muted, :wikibanned, :contributors, :wikicontributors, :moderators]
133
+ # the type of listing
134
+ # @param params [Hash] a list of params to send with the request
135
+ # @option params [String] :after return results after the given fullname
136
+ # @option params [String] :before return results before the given fullname
137
+ # @option params [Integer] :count the number of items already seen in the listing
138
+ # @option params [1..100] :limit the maximum number of things to return
139
+ # @option params [String] :user find a specific user
140
+ #
141
+ # @return [Array<Hash>]
142
+ def relationship_listing(type, **params)
143
+ # TODO: add methods to determine if a certain user was banned/muted/etc
144
+ user_list = @client.get("/r/#{get_attribute(:display_name)}/about/#{type}", params).body
145
+ user_list[:data][:children]
146
+ end
147
+
148
+ # @!method banned(**params)
149
+ # @!method muted(**params)
150
+ # @!method wikibanned(**params)
151
+ # @!method contributors(**params)
152
+ # @!method wikicontributors(**params)
153
+ # @!method moderators(**params)
154
+ #
155
+ # @see #relationship_listing
156
+ %i(banned muted wikibanned contributors wikicontributors moderators).each do |type|
157
+ define_method(type) { |**params| relationship_listing(type, **params) }
158
+ end
159
+
100
160
  # @!endgroup
101
161
 
102
162
  # Stream newly submitted posts.
@@ -196,6 +256,50 @@ module Redd
196
256
  def unsubscribe
197
257
  subscribe(action: :unsub)
198
258
  end
259
+
260
+ # Get the subreddit's CSS.
261
+ # @return [String, nil] the stylesheet or nil if no stylesheet exists
262
+ def stylesheet
263
+ url = @client.get("/r/#{get_attribute(:display_name)}/stylesheet").headers['location']
264
+ HTTP.get(url).body.to_s
265
+ rescue Redd::NotFound
266
+ nil
267
+ end
268
+
269
+ # Edit the subreddit's stylesheet.
270
+ # @param text [String] the updated CSS
271
+ # @param reason [String] the reason for modifying the stylesheet
272
+ def update_stylesheet(text, reason: nil)
273
+ params = { op: 'save', stylesheet_contents: text }
274
+ params[:reason] = reason if reason
275
+ @client.post("/r/#{get_attribute(:display_name)}/api/subreddit_stylesheet", params)
276
+ end
277
+
278
+ # @return [Hash] the subreddit's settings
279
+ def settings
280
+ @client.get("/r/#{get_attribute(:display_name)}/about/edit").body[:data]
281
+ end
282
+
283
+ # Modify the subreddit's settings.
284
+ # @param params [Hash] the settings to change
285
+ # @see https://www.reddit.com/dev/api#POST_api_site_admin
286
+ def modify_settings(**params)
287
+ full_params = settings.merge(params)
288
+ full_params[:sr] = get_attribute(:name)
289
+ SETTINGS_MAP.each { |src, dest| full_params[dest] = full_params.delete(src) }
290
+ @client.post('/api/site_admin', full_params)
291
+ end
292
+
293
+ private
294
+
295
+ def add_relationship(**params)
296
+ # FIXME: add public methods
297
+ @client.post("/r/#{get_attribute(:display_name)}/api/friend", params)
298
+ end
299
+
300
+ def remove_relationship(**params)
301
+ @client.post("/r/#{get_attribute(:display_name)}/api/unfriend", params)
302
+ end
199
303
  end
200
304
  end
201
305
  end
@@ -34,6 +34,20 @@ module Redd
34
34
  super(to: get_attribute(:name), subject: subject, text: text, from: from)
35
35
  end
36
36
 
37
+ # Add the user as a friend.
38
+ # @param note [String] a note for the friend
39
+ def friend(note = nil)
40
+ name = get_attribute(:name)
41
+ body = JSON.generate(note ? { name: name, note: note } : { name: name })
42
+ @client.request(:put, "/api/v1/me/friends/#{name}", body: body)
43
+ end
44
+
45
+ # Unfriend the user.
46
+ def unfriend
47
+ name = get_attribute(:name)
48
+ @client.request(:delete, "/api/v1/me/friends/#{name}", raw: true, form: { id: name })
49
+ end
50
+
37
51
  # Get the appropriate listing.
38
52
  # @param type [:overview, :submitted, :comments, :liked, :disliked, :hidden, :saved, :gilded]
39
53
  # the type of listing to request
@@ -5,10 +5,10 @@ require_relative '../error'
5
5
  module Redd
6
6
  module Utilities
7
7
  # Handles response errors in API responses.
8
- # TODO: handle [:json][:errors] array
9
8
  class ErrorHandler
10
9
  HTTP_ERRORS = {
11
10
  400 => Redd::BadRequest,
11
+ 403 => Redd::Forbidden,
12
12
  404 => Redd::NotFound,
13
13
  500 => Redd::ServerError,
14
14
  502 => Redd::ServerError,
@@ -21,14 +21,19 @@ module Redd
21
21
  'invalid_token' => Redd::InvalidAccess
22
22
  }.freeze
23
23
 
24
- def check_error(response)
25
- return HTTP_ERRORS[response.code].new(response) if HTTP_ERRORS.key?(response.code)
26
- if response.code == 403
24
+ def check_error(response, raw:)
25
+ # TODO: deal with errors of type { fields:, explanation:, message:, reason: }
26
+ if !raw && response.body[:json] && response.body[:json][:errors] &&
27
+ !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
27
32
  AUTHORIZATION_ERRORS.each do |key, klass|
28
- return klass.new(response) if response.headers['www-authenticate'].include?(key)
33
+ auth_header = response.headers['www-authenticate']
34
+ return klass.new(response) if auth_header && auth_header.include?(key)
29
35
  end
30
36
  end
31
- nil
32
37
  end
33
38
  end
34
39
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'enumerator'
4
-
5
3
  module Redd
6
4
  module Utilities
7
5
  # A forward-expading listing of items that can be enumerated forever.
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.0.pre.1'
4
+ VERSION = '0.8.0.pre.2'
5
5
  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.0.pre.1
4
+ version: 0.8.0.pre.2
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-02-25 00:00:00.000000000 Z
11
+ date: 2017-02-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http
@@ -163,6 +163,7 @@ files:
163
163
  - lib/redd/models/postable.rb
164
164
  - lib/redd/models/private_message.rb
165
165
  - lib/redd/models/replyable.rb
166
+ - lib/redd/models/searchable.rb
166
167
  - lib/redd/models/session.rb
167
168
  - lib/redd/models/submission.rb
168
169
  - lib/redd/models/subreddit.rb