redd 0.7.8 → 0.7.9
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 +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
|