neonredd 0.0.1
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 +7 -0
- data/Gemfile +4 -0
- data/LICENSE.md +22 -0
- data/README.md +1 -0
- data/Rakefile +5 -0
- data/Redd.LICENSE.md +22 -0
- data/RedditKit.LICENSE.md +9 -0
- data/lib/redd/access.rb +76 -0
- data/lib/redd/clients/base/account.rb +20 -0
- data/lib/redd/clients/base/identity.rb +22 -0
- data/lib/redd/clients/base/none.rb +27 -0
- data/lib/redd/clients/base/privatemessages.rb +33 -0
- data/lib/redd/clients/base/read.rb +114 -0
- data/lib/redd/clients/base/stream.rb +82 -0
- data/lib/redd/clients/base/submit.rb +19 -0
- data/lib/redd/clients/base/utilities.rb +143 -0
- data/lib/redd/clients/base/wikiread.rb +33 -0
- data/lib/redd/clients/base.rb +181 -0
- data/lib/redd/clients/installed.rb +56 -0
- data/lib/redd/clients/script.rb +40 -0
- data/lib/redd/clients/userless.rb +32 -0
- data/lib/redd/clients/web.rb +59 -0
- data/lib/redd/error.rb +151 -0
- data/lib/redd/objects/base.rb +39 -0
- data/lib/redd/objects/comment.rb +22 -0
- data/lib/redd/objects/labeled_multi.rb +13 -0
- data/lib/redd/objects/listing.rb +29 -0
- data/lib/redd/objects/more_comments.rb +10 -0
- data/lib/redd/objects/private_message.rb +28 -0
- data/lib/redd/objects/submission.rb +140 -0
- data/lib/redd/objects/subreddit.rb +329 -0
- data/lib/redd/objects/thing/editable.rb +22 -0
- data/lib/redd/objects/thing/hideable.rb +18 -0
- data/lib/redd/objects/thing/inboxable.rb +25 -0
- data/lib/redd/objects/thing/messageable.rb +38 -0
- data/lib/redd/objects/thing/moderatable.rb +43 -0
- data/lib/redd/objects/thing/refreshable.rb +14 -0
- data/lib/redd/objects/thing/saveable.rb +21 -0
- data/lib/redd/objects/thing/votable.rb +33 -0
- data/lib/redd/objects/thing.rb +26 -0
- data/lib/redd/objects/user.rb +52 -0
- data/lib/redd/objects/wiki_page.rb +15 -0
- data/lib/redd/rate_limit.rb +88 -0
- data/lib/redd/response/parse_json.rb +18 -0
- data/lib/redd/response/raise_error.rb +16 -0
- data/lib/redd/version.rb +4 -0
- data/lib/redd.rb +50 -0
- data/neonredd.gemspec +33 -0
- data/spec/redd/objects/base_spec.rb +1 -0
- data/spec/redd/response/raise_error_spec.rb +11 -0
- data/spec/redd_spec.rb +5 -0
- data/spec/spec_helper.rb +71 -0
- metadata +225 -0
@@ -0,0 +1,181 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require_relative '../version'
|
3
|
+
require_relative '../response/raise_error'
|
4
|
+
require_relative '../response/parse_json'
|
5
|
+
require_relative '../rate_limit'
|
6
|
+
require_relative '../access'
|
7
|
+
|
8
|
+
module Redd
|
9
|
+
# The module containing the multiple types of clients.
|
10
|
+
module Clients
|
11
|
+
# The basic client to inherit from. Don't use this directly, prefer
|
12
|
+
# {Redd.it}
|
13
|
+
class Base
|
14
|
+
# @!parse include Utilities
|
15
|
+
# @!parse include Account
|
16
|
+
# @!parse include Identity
|
17
|
+
# @!parse include None
|
18
|
+
# @!parse include Privatemessages
|
19
|
+
# @!parse include Read
|
20
|
+
# @!parse include Submit
|
21
|
+
# @!parse include Stream
|
22
|
+
# @!parse include Wikiread
|
23
|
+
%w(
|
24
|
+
utilities account identity none privatemessages read submit stream
|
25
|
+
wikiread
|
26
|
+
).each do |mixin_name|
|
27
|
+
require_relative "base/#{mixin_name}"
|
28
|
+
camel_case = mixin_name.split('_').map(&:capitalize).join
|
29
|
+
include const_get(camel_case)
|
30
|
+
end
|
31
|
+
|
32
|
+
# @!attribute [r] user_agent
|
33
|
+
# @return [String] The user-agent used to communicate with reddit.
|
34
|
+
attr_reader :user_agent
|
35
|
+
|
36
|
+
# @!attribute [r] rate_limit
|
37
|
+
# @return [#after_limit] The handler that takes care of rate limiting.
|
38
|
+
attr_reader :rate_limit
|
39
|
+
|
40
|
+
# @!attribute [r] auth_endpoint
|
41
|
+
# @return [String] The site to connect to for authentication.
|
42
|
+
attr_reader :auth_endpoint
|
43
|
+
|
44
|
+
# @!attribute [r] api_endpoint
|
45
|
+
# @return [String] The site to make API requests with.
|
46
|
+
attr_reader :api_endpoint
|
47
|
+
|
48
|
+
# @!attribute [rw] access
|
49
|
+
# @return [Access] The access object to make API requests with.
|
50
|
+
attr_accessor :access
|
51
|
+
|
52
|
+
# Create a Client.
|
53
|
+
#
|
54
|
+
# @param [Hash] options The options to create the client with.
|
55
|
+
# @option options [String] :user_agent The User-Agent string to use in
|
56
|
+
# the header of every request.
|
57
|
+
# @option options [#after_limit] :rate_limit The handler that takes care
|
58
|
+
# of rate limiting.
|
59
|
+
# @option options [String] :auth_endpoint The main domain to authenticate
|
60
|
+
# with.
|
61
|
+
# @option options [String] :api_endpoint The main domain to make requests
|
62
|
+
# with.
|
63
|
+
# @note HTTPS is mandatory for OAuth2.
|
64
|
+
def initialize(**options)
|
65
|
+
@user_agent = options[:user_agent] || "Redd/Ruby, v#{Redd::VERSION}"
|
66
|
+
@rate_limit = options[:rate_limit] || RateLimit.new(1)
|
67
|
+
@auth_endpoint = options[:auth_endpoint] || 'https://www.reddit.com/'
|
68
|
+
@api_endpoint = options[:api_endpoint] || 'https://oauth.reddit.com/'
|
69
|
+
@access = Access.new(expires_at: Time.at(0))
|
70
|
+
end
|
71
|
+
|
72
|
+
# @!method get(path, params = {})
|
73
|
+
# @!method post(path, params = {})
|
74
|
+
# @!method put(path, params = {})
|
75
|
+
# @!method patch(path, params = {})
|
76
|
+
# @!method delete(path, params = {})
|
77
|
+
#
|
78
|
+
# Sends the request to the given path with the given params and return
|
79
|
+
# the body of the response.
|
80
|
+
# @param [String] path The path under the api_endpoint to request.
|
81
|
+
# @param [Hash] params The parameters to send with the request.
|
82
|
+
# @return [Faraday::Response] The response.
|
83
|
+
[:get, :post, :put, :patch, :delete].each do |meth|
|
84
|
+
define_method(meth) do |path, params = {}|
|
85
|
+
@rate_limit.after_limit do
|
86
|
+
final_params = default_params.merge(params)
|
87
|
+
connection.send(meth, path, final_params)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# @param [Access] new_access The access to use.
|
93
|
+
# @yield The client with the given access.
|
94
|
+
def with(new_access)
|
95
|
+
old_access = @access
|
96
|
+
@access = new_access
|
97
|
+
response = yield(self)
|
98
|
+
@access = old_access
|
99
|
+
response
|
100
|
+
end
|
101
|
+
|
102
|
+
# Obtain a new access token using a refresh token.
|
103
|
+
# @return [Access] The refreshed information.
|
104
|
+
def refresh_access!
|
105
|
+
response = auth_connection.post(
|
106
|
+
'/api/v1/access_token',
|
107
|
+
grant_type: 'refresh_token',
|
108
|
+
refresh_token: access.refresh_token
|
109
|
+
)
|
110
|
+
access.refreshed!(response.body)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Dispose of an access or refresh token when you're done with it.
|
114
|
+
# @param [Boolean] remove_refresh_token Whether or not to remove all
|
115
|
+
# tokens associated with the user.
|
116
|
+
def revoke_access!(remove_refresh_token = false)
|
117
|
+
token_type = remove_refresh_token ? :refresh_token : :access_token
|
118
|
+
token = access.send(token_type)
|
119
|
+
@access = nil
|
120
|
+
auth_connection.post(
|
121
|
+
'/api/v1/revoke_token',
|
122
|
+
token: token,
|
123
|
+
token_type_hint: token_type
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
# @return [Faraday::RackBuilder] The middleware to use when creating the
|
130
|
+
# connection.
|
131
|
+
def middleware
|
132
|
+
@middleware ||= Faraday::RackBuilder.new do |builder|
|
133
|
+
builder.use Response::RaiseError
|
134
|
+
builder.use Response::ParseJson
|
135
|
+
builder.use Faraday::Request::Multipart
|
136
|
+
builder.use Faraday::Request::UrlEncoded
|
137
|
+
builder.adapter Faraday.default_adapter
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# @return [Hash] The minimum parameters to send with every request.
|
142
|
+
def default_params
|
143
|
+
@default_params ||= { api_type: 'json' }
|
144
|
+
end
|
145
|
+
|
146
|
+
# @return [Hash] A hash of the headers used.
|
147
|
+
def default_headers
|
148
|
+
{
|
149
|
+
'User-Agent' => @user_agent,
|
150
|
+
'Authorization' => "bearer #{@access.access_token}"
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
# @return [Faraday::Connection] A new or existing connection.
|
155
|
+
def connection
|
156
|
+
@connection ||= Faraday.new(
|
157
|
+
@api_endpoint,
|
158
|
+
headers: default_headers,
|
159
|
+
builder: middleware
|
160
|
+
)
|
161
|
+
end
|
162
|
+
|
163
|
+
# @return [Hash] A hash of the headers with basic auth.
|
164
|
+
def auth_headers
|
165
|
+
{
|
166
|
+
'User-Agent' => @user_agent,
|
167
|
+
'Authorization' => Faraday.basic_auth(@client_id, @secret)
|
168
|
+
}
|
169
|
+
end
|
170
|
+
|
171
|
+
# @return [Faraday::Connection] A new or existing connection.
|
172
|
+
def auth_connection
|
173
|
+
@auth_connection ||= Faraday.new(
|
174
|
+
@auth_endpoint,
|
175
|
+
headers: auth_headers,
|
176
|
+
builder: middleware
|
177
|
+
)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require_relative 'base'
|
3
|
+
|
4
|
+
module Redd
|
5
|
+
module Clients
|
6
|
+
# The client for installed apps that can't keep a secret.
|
7
|
+
# It might even work with Rubymotion (fingers crossed).
|
8
|
+
class Installed < Base
|
9
|
+
# @!attribute [r] client_id
|
10
|
+
attr_reader :client_id
|
11
|
+
|
12
|
+
# @!attribute [r] redirect_uri
|
13
|
+
attr_reader :redirect_uri
|
14
|
+
|
15
|
+
# @param [Hash] options The options to create the client with.
|
16
|
+
# @see Base#initialize
|
17
|
+
# @see Redd.it
|
18
|
+
def initialize(client_id, redirect_uri, **options)
|
19
|
+
@client_id = client_id
|
20
|
+
@redirect_uri = redirect_uri
|
21
|
+
super(**options)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param [String] state A random string to double-check later.
|
25
|
+
# @param [Array<String>] scope The scope to request access to.
|
26
|
+
# @param [:temporary, :permanent] duration
|
27
|
+
# @return [String] The url to redirect the user to.
|
28
|
+
# rubocop:disable Metrics/MethodLength
|
29
|
+
def auth_url(state, scope = ['identity'], duration = :temporary)
|
30
|
+
query = {
|
31
|
+
response_type: 'token',
|
32
|
+
client_id: @client_id,
|
33
|
+
redirect_uri: @redirect_uri,
|
34
|
+
state: state,
|
35
|
+
scope: scope.join(','),
|
36
|
+
duration: duration
|
37
|
+
}
|
38
|
+
url = URI.join(auth_endpoint, '/api/v1/authorize')
|
39
|
+
url.query = URI.encode_www_form(query)
|
40
|
+
url.to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
# Authorize using the url fragment.
|
44
|
+
# @param [String] fragment The part of the url after the "#".
|
45
|
+
# @return [Access] The access given by reddit.
|
46
|
+
def authorize!(fragment)
|
47
|
+
parsed = CGI.parse(fragment)
|
48
|
+
@access = Access.new(
|
49
|
+
access_token: parsed[:access_token].first,
|
50
|
+
expires_in: parsed[:expires_in].first,
|
51
|
+
scope: parsed[:scope]
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Redd
|
4
|
+
module Clients
|
5
|
+
# The client for an account you own (e.g. bots).
|
6
|
+
class Script < Base
|
7
|
+
# @!attribute [r] client_id
|
8
|
+
attr_reader :client_id
|
9
|
+
|
10
|
+
# @!attribute [r] username
|
11
|
+
attr_reader :username
|
12
|
+
|
13
|
+
# @param [Hash] options The options to create the client with.
|
14
|
+
# @see Base#initialize
|
15
|
+
# @see Redd.it
|
16
|
+
def initialize(client_id, secret, username, password, **options)
|
17
|
+
@client_id = client_id
|
18
|
+
@secret = secret
|
19
|
+
@username = username
|
20
|
+
@password = password
|
21
|
+
super(**options)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Authorize using the given data.
|
25
|
+
# @return [Access] The access given by reddit.
|
26
|
+
def authorize!
|
27
|
+
response = auth_connection.post(
|
28
|
+
'/api/v1/access_token',
|
29
|
+
grant_type: 'password',
|
30
|
+
username: @username,
|
31
|
+
password: @password
|
32
|
+
)
|
33
|
+
|
34
|
+
@access = Access.new(response.body)
|
35
|
+
end
|
36
|
+
|
37
|
+
alias refresh_access! authorize!
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Redd
|
4
|
+
module Clients
|
5
|
+
# The client that doesn't need a user to function.
|
6
|
+
# @note Of course, that means many editing methods throw an error.
|
7
|
+
class Userless < Base
|
8
|
+
# @!attribute [r] client_id
|
9
|
+
attr_reader :client_id
|
10
|
+
|
11
|
+
# @param [Hash] options The options to create the client with.
|
12
|
+
# @see Base#initialize
|
13
|
+
# @see Redd.it
|
14
|
+
def initialize(client_id, secret, **options)
|
15
|
+
@client_id = client_id
|
16
|
+
@secret = secret
|
17
|
+
super(**options)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Authorize using the given data.
|
21
|
+
# @return [Access] The access given by reddit.
|
22
|
+
def authorize!
|
23
|
+
response = auth_connection.post(
|
24
|
+
'/api/v1/access_token',
|
25
|
+
grant_type: 'client_credentials'
|
26
|
+
)
|
27
|
+
|
28
|
+
@access = Access.new(response.body)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require_relative 'base'
|
3
|
+
|
4
|
+
module Redd
|
5
|
+
module Clients
|
6
|
+
# The client for a web-based flow (e.g. "login with reddit")
|
7
|
+
class Web < Base
|
8
|
+
# @!attribute [r] client_id
|
9
|
+
attr_reader :client_id
|
10
|
+
|
11
|
+
# @!attribute [r] redirect_uri
|
12
|
+
attr_reader :redirect_uri
|
13
|
+
|
14
|
+
# @param [Hash] options The options to create the client with.
|
15
|
+
# @see Base#initialize
|
16
|
+
# @see Redd.it
|
17
|
+
def initialize(client_id, secret, redirect_uri, **options)
|
18
|
+
@client_id = client_id
|
19
|
+
@secret = secret
|
20
|
+
@redirect_uri = redirect_uri
|
21
|
+
super(**options)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param [String] state A random string to double-check later.
|
25
|
+
# @param [Array<String>] scope The scope to request access to.
|
26
|
+
# @param [:temporary, :permanent] duration
|
27
|
+
# @return [String] The url to redirect the user to.
|
28
|
+
# rubocop:disable Metrics/MethodLength
|
29
|
+
def auth_url(state, scope = ['identity'], duration = :temporary)
|
30
|
+
query = {
|
31
|
+
response_type: 'code',
|
32
|
+
client_id: @client_id,
|
33
|
+
redirect_uri: @redirect_uri,
|
34
|
+
state: state,
|
35
|
+
scope: scope.join(','),
|
36
|
+
duration: duration
|
37
|
+
}
|
38
|
+
|
39
|
+
url = URI.join(auth_endpoint, '/api/v1/authorize')
|
40
|
+
url.query = URI.encode_www_form(query)
|
41
|
+
url.to_s
|
42
|
+
end
|
43
|
+
|
44
|
+
# Authorize using the code given.
|
45
|
+
# @param [String] code The code from the get params.
|
46
|
+
# @return [Access] The access given by reddit.
|
47
|
+
def authorize!(code)
|
48
|
+
response = auth_connection.post(
|
49
|
+
'/api/v1/access_token',
|
50
|
+
grant_type: 'authorization_code',
|
51
|
+
code: code,
|
52
|
+
redirect_uri: @redirect_uri
|
53
|
+
)
|
54
|
+
|
55
|
+
@access = Access.new(response.body)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/redd/error.rb
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
module Redd
|
2
|
+
# An error from reddit
|
3
|
+
# TODO: Move Error to an Errors module in next minor version?
|
4
|
+
class Error < StandardError
|
5
|
+
attr_reader :code
|
6
|
+
attr_reader :headers
|
7
|
+
attr_reader :body
|
8
|
+
|
9
|
+
def initialize(env)
|
10
|
+
@code = env[:status]
|
11
|
+
@headers = env[:response_headers]
|
12
|
+
@body = env[:body]
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.from_response(env) # rubocop:disable all
|
16
|
+
status = env[:status]
|
17
|
+
body = parse_error(env[:body]).to_s
|
18
|
+
case status
|
19
|
+
when 200
|
20
|
+
case body
|
21
|
+
when /access_denied/i then OAuth2AccessDenied
|
22
|
+
when /unsupported_response_type/i then InvalidResponseType
|
23
|
+
when /unsupported_grant_type/i then InvalidGrantType
|
24
|
+
when /invalid_scope/i then InvalidScope
|
25
|
+
when /invalid_request/i then InvalidRequest
|
26
|
+
when /no_text/i then NoTokenGiven
|
27
|
+
when /invalid_grant/i then ExpiredCode
|
28
|
+
when /wrong_password/i then InvalidCredentials
|
29
|
+
when /bad_captcha/i then InvalidCaptcha
|
30
|
+
when /ratelimit/i then RateLimited
|
31
|
+
when /quota_filled/i then QuotaFilled
|
32
|
+
when /bad_css_name/i then InvalidClassName
|
33
|
+
when /too_old/i then Archived
|
34
|
+
when /too_much_flair_css/i then TooManyClassNames
|
35
|
+
when /user_required/i then AuthenticationRequired
|
36
|
+
end
|
37
|
+
when 400 then BadRequest
|
38
|
+
when 401 then InvalidOAuth2Credentials
|
39
|
+
when 403
|
40
|
+
if /user_required/i =~ body
|
41
|
+
AuthenticationRequired
|
42
|
+
else
|
43
|
+
PermissionDenied
|
44
|
+
end
|
45
|
+
when 404 then NotFound
|
46
|
+
when 409 then Conflict
|
47
|
+
when 500 then InternalServerError
|
48
|
+
when 502 then BadGateway
|
49
|
+
when 503 then ServiceUnavailable
|
50
|
+
when 504 then TimedOut
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.parse_error(body) # rubocop:disable all
|
55
|
+
return body unless body.is_a?(Hash)
|
56
|
+
|
57
|
+
if body.key?(:json) && body[:json].key?(:errors)
|
58
|
+
body[:json][:errors].first
|
59
|
+
elsif body.key?(:jquery)
|
60
|
+
body[:jquery]
|
61
|
+
elsif body.key?(:error)
|
62
|
+
body[:error]
|
63
|
+
elsif body.key?(:code) && body[:code] == 'NO_TEXT'
|
64
|
+
'NO_TEXT'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# This item has been archived and can no longer be edited.
|
69
|
+
Archived = Class.new(Error)
|
70
|
+
|
71
|
+
AuthenticationRequired = Class.new(Error)
|
72
|
+
|
73
|
+
# Bad Gateway. Either a network or a reddit error. Either way, try again.
|
74
|
+
BadGateway = Class.new(Error)
|
75
|
+
|
76
|
+
BadRequest = Class.new(Error)
|
77
|
+
|
78
|
+
Conflict = Class.new(Error)
|
79
|
+
|
80
|
+
# You already received an access token using this code. The user should
|
81
|
+
# grant you access again to get a new code.
|
82
|
+
ExpiredCode = Class.new(Error)
|
83
|
+
|
84
|
+
# There is an issue on reddit's end. Try again.
|
85
|
+
InternalServerError = Class.new(Error)
|
86
|
+
|
87
|
+
InvalidCaptcha = Class.new(Error)
|
88
|
+
|
89
|
+
InvalidClassName = Class.new(Error)
|
90
|
+
|
91
|
+
# Either your username or your password is wrong.
|
92
|
+
InvalidCredentials = Class.new(Error)
|
93
|
+
|
94
|
+
InvalidGrantType = Class.new(Error)
|
95
|
+
|
96
|
+
InvalidMultiredditName = Class.new(Error)
|
97
|
+
|
98
|
+
# Your client id or your secret is wrong.
|
99
|
+
InvalidOAuth2Credentials = Class.new(Error)
|
100
|
+
|
101
|
+
InvalidResponseType = Class.new(Error)
|
102
|
+
|
103
|
+
InvalidRequest = Class.new(Error)
|
104
|
+
|
105
|
+
# You don't have the proper scope to perform this request.
|
106
|
+
InvalidScope = Class.new(Error)
|
107
|
+
|
108
|
+
# Looks like we didn't get a JSON response. Raise this error.
|
109
|
+
JSONError = Class.new(Error)
|
110
|
+
|
111
|
+
# Four, oh four! The thing you're looking for wasn't found.
|
112
|
+
NotFound = Class.new(Error)
|
113
|
+
|
114
|
+
# No access token was given.
|
115
|
+
NoTokenGiven = Class.new(Error)
|
116
|
+
|
117
|
+
OAuth2AccessDenied = Class.new(Error)
|
118
|
+
|
119
|
+
PermissionDenied = Class.new(Error)
|
120
|
+
|
121
|
+
# Raised when the client needs to wait before making another request
|
122
|
+
class RateLimited < Error
|
123
|
+
# @!attribute [r] time
|
124
|
+
# @return [Integer] the seconds to wait before making another request.
|
125
|
+
attr_reader :time
|
126
|
+
|
127
|
+
def initialize(env)
|
128
|
+
super
|
129
|
+
@time = env[:body][:json][:ratelimit] || 3600
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# This seems to be a more OAuth2-focused error.
|
134
|
+
class QuotaFilled < RateLimited
|
135
|
+
def initialize(env)
|
136
|
+
super
|
137
|
+
@time = 3600
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
RequestError = Class.new(Error)
|
142
|
+
|
143
|
+
# Issue on reddit's end. Try again.
|
144
|
+
ServiceUnavailable = Class.new(Error)
|
145
|
+
|
146
|
+
# The connection timed out. Try again.
|
147
|
+
TimedOut = Class.new(Error)
|
148
|
+
|
149
|
+
TooManyClassNames = Class.new(Error)
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'hashie'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Redd
|
5
|
+
# A bunch of objects that can hold properties.
|
6
|
+
module Objects
|
7
|
+
# A base for all objects to inherit from.
|
8
|
+
class Base < Hashie::Hash
|
9
|
+
include Hashie::Extensions::MergeInitializer
|
10
|
+
include Hashie::Extensions::MethodReader
|
11
|
+
include Hashie::Extensions::MethodQuery
|
12
|
+
include Hashie::Extensions::DeepMerge
|
13
|
+
|
14
|
+
# The `delete` method is called `delete_path` because it conflicts with
|
15
|
+
# Hash#delete.
|
16
|
+
extend Forwardable
|
17
|
+
def_delegators :@client, :get, :post, :put, :delete_path
|
18
|
+
|
19
|
+
# @!attribute [r] client
|
20
|
+
# @return [Clients::Base] The client that used to make requests.
|
21
|
+
attr_reader :client
|
22
|
+
|
23
|
+
# @param [Clients::Base] client The client instance.
|
24
|
+
# @param [Hash] attributes A hash of attributes.
|
25
|
+
def initialize(client, attributes = {})
|
26
|
+
@client = client
|
27
|
+
super(attributes)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Define an alias for a property.
|
31
|
+
# @param [Symbol] new_name The alias.
|
32
|
+
# @param [Symbol] old_name The existing property.
|
33
|
+
def self.alias_property(new_name, old_name)
|
34
|
+
define_method(new_name) { send(old_name) }
|
35
|
+
define_method(:"#{new_name}?") { send(old_name) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative 'thing'
|
2
|
+
|
3
|
+
module Redd
|
4
|
+
module Objects
|
5
|
+
# A comment that can be made on a link.
|
6
|
+
class Comment < Thing
|
7
|
+
include Thing::Editable
|
8
|
+
include Thing::Inboxable
|
9
|
+
include Thing::Moderatable
|
10
|
+
include Thing::Refreshable
|
11
|
+
include Thing::Saveable
|
12
|
+
include Thing::Votable
|
13
|
+
|
14
|
+
alias_property :reports_count, :num_reports
|
15
|
+
|
16
|
+
# @return [Listing] The comment's replies.
|
17
|
+
def replies
|
18
|
+
@replies ||= (client.object_from_body(self[:replies]) || [])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Redd
|
2
|
+
module Objects
|
3
|
+
# A comment that can be made on a link.
|
4
|
+
class LabeledMulti < Base
|
5
|
+
# @see Objects::Base
|
6
|
+
def initialize(client, attributes = {})
|
7
|
+
attr_dup = attributes.dup
|
8
|
+
attr_dup[:subreddits].map! { |sub| sub[:name] }
|
9
|
+
super(client, attr_dup)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Redd
|
2
|
+
module Objects
|
3
|
+
# A collection of reddit things.
|
4
|
+
# @see https://www.reddit.com/dev/api#listings
|
5
|
+
class Listing < Array
|
6
|
+
# @!attribute [r] before
|
7
|
+
# @return [String] The id of the object before the listing.
|
8
|
+
attr_reader :before
|
9
|
+
|
10
|
+
# @!attribute [r] after
|
11
|
+
# @return [String] The id of the object after the listing.
|
12
|
+
attr_reader :after
|
13
|
+
|
14
|
+
# @param [Clients::Base] client The client to expand the comments with.
|
15
|
+
# @param [{:before => String, :after => String,
|
16
|
+
# :children => Array<Hash>}] attributes The data to initialize the
|
17
|
+
# class with.
|
18
|
+
# @todo Only call Clients::Base#object_from_body when item is being
|
19
|
+
# accessed.
|
20
|
+
def initialize(client, attributes)
|
21
|
+
@before = attributes[:before]
|
22
|
+
@after = attributes[:after]
|
23
|
+
attributes[:children].each do |child|
|
24
|
+
self << (client.object_from_body(child) || child)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative 'thing'
|
2
|
+
|
3
|
+
module Redd
|
4
|
+
module Objects
|
5
|
+
# The model for private messages
|
6
|
+
class PrivateMessage < Thing
|
7
|
+
include Thing::Inboxable
|
8
|
+
|
9
|
+
alias_property :from, :author
|
10
|
+
alias_property :to, :dest
|
11
|
+
|
12
|
+
# Block the sender of the message from sending any more.
|
13
|
+
def block_sender!
|
14
|
+
post('/api/block', id: fullname)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Mark the message as read.
|
18
|
+
def mark_as_read
|
19
|
+
post('/api/read_message', id: fullname)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Mark the message as unread and add orangered to account.
|
23
|
+
def mark_as_unread
|
24
|
+
post('/api/unread_message', id: fullname)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|