neonredd 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|