redd 0.7.8 → 0.7.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +34 -34
- data/.rspec +3 -3
- data/.rubocop.yml +5 -5
- data/.travis.yml +9 -9
- data/LICENSE.md +22 -22
- data/README.md +143 -143
- data/Rakefile +5 -5
- data/RedditKit.LICENSE.md +8 -8
- data/lib/redd.rb +50 -50
- data/lib/redd/access.rb +76 -76
- data/lib/redd/clients/base.rb +181 -181
- data/lib/redd/clients/base/account.rb +20 -20
- data/lib/redd/clients/base/identity.rb +22 -22
- data/lib/redd/clients/base/none.rb +27 -27
- data/lib/redd/clients/base/privatemessages.rb +33 -33
- data/lib/redd/clients/base/read.rb +113 -113
- data/lib/redd/clients/base/stream.rb +81 -81
- data/lib/redd/clients/base/submit.rb +19 -19
- data/lib/redd/clients/base/utilities.rb +104 -104
- data/lib/redd/clients/base/wikiread.rb +33 -33
- data/lib/redd/clients/installed.rb +56 -56
- data/lib/redd/clients/script.rb +41 -41
- data/lib/redd/clients/userless.rb +32 -32
- data/lib/redd/clients/web.rb +58 -58
- data/lib/redd/error.rb +151 -151
- data/lib/redd/objects/base.rb +39 -39
- data/lib/redd/objects/comment.rb +22 -22
- data/lib/redd/objects/labeled_multi.rb +13 -13
- data/lib/redd/objects/listing.rb +29 -29
- data/lib/redd/objects/more_comments.rb +11 -10
- data/lib/redd/objects/private_message.rb +28 -28
- data/lib/redd/objects/submission.rb +139 -139
- data/lib/redd/objects/subreddit.rb +330 -319
- data/lib/redd/objects/thing.rb +26 -26
- data/lib/redd/objects/thing/editable.rb +22 -22
- data/lib/redd/objects/thing/hideable.rb +18 -18
- data/lib/redd/objects/thing/inboxable.rb +25 -25
- data/lib/redd/objects/thing/messageable.rb +34 -34
- data/lib/redd/objects/thing/moderatable.rb +43 -43
- data/lib/redd/objects/thing/refreshable.rb +14 -14
- data/lib/redd/objects/thing/saveable.rb +21 -21
- data/lib/redd/objects/thing/votable.rb +33 -33
- data/lib/redd/objects/user.rb +52 -52
- data/lib/redd/objects/wiki_page.rb +15 -15
- data/lib/redd/rate_limit.rb +88 -88
- data/lib/redd/response/parse_json.rb +18 -18
- data/lib/redd/response/raise_error.rb +16 -16
- data/lib/redd/version.rb +4 -4
- data/redd.gemspec +31 -31
- data/spec/redd/objects/base_spec.rb +1 -1
- data/spec/redd/response/raise_error_spec.rb +11 -11
- data/spec/redd_spec.rb +5 -5
- data/spec/spec_helper.rb +71 -71
- metadata +21 -21
@@ -1,32 +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
|
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
|
data/lib/redd/clients/web.rb
CHANGED
@@ -1,58 +1,58 @@
|
|
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
|
-
def auth_url(state, scope = ["identity"], duration = :temporary)
|
29
|
-
query = {
|
30
|
-
response_type: "code",
|
31
|
-
client_id: @client_id,
|
32
|
-
redirect_uri: @redirect_uri,
|
33
|
-
state: state,
|
34
|
-
scope: scope.join(","),
|
35
|
-
duration: duration
|
36
|
-
}
|
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 code given.
|
44
|
-
# @param [String] code The code from the get params.
|
45
|
-
# @return [Access] The access given by reddit.
|
46
|
-
def authorize!(code)
|
47
|
-
response = auth_connection.post(
|
48
|
-
"/api/v1/access_token",
|
49
|
-
grant_type: "authorization_code",
|
50
|
-
code: code,
|
51
|
-
redirect_uri: @redirect_uri
|
52
|
-
)
|
53
|
-
|
54
|
-
@access = Access.new(response.body)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
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
|
+
def auth_url(state, scope = ["identity"], duration = :temporary)
|
29
|
+
query = {
|
30
|
+
response_type: "code",
|
31
|
+
client_id: @client_id,
|
32
|
+
redirect_uri: @redirect_uri,
|
33
|
+
state: state,
|
34
|
+
scope: scope.join(","),
|
35
|
+
duration: duration
|
36
|
+
}
|
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 code given.
|
44
|
+
# @param [String] code The code from the get params.
|
45
|
+
# @return [Access] The access given by reddit.
|
46
|
+
def authorize!(code)
|
47
|
+
response = auth_connection.post(
|
48
|
+
"/api/v1/access_token",
|
49
|
+
grant_type: "authorization_code",
|
50
|
+
code: code,
|
51
|
+
redirect_uri: @redirect_uri
|
52
|
+
)
|
53
|
+
|
54
|
+
@access = Access.new(response.body)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/redd/error.rb
CHANGED
@@ -1,151 +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
|
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
|