redd 0.8.1 → 0.8.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +18 -8
- data/bin/console +42 -16
- data/lib/redd.rb +50 -19
- data/lib/redd/api_client.rb +18 -27
- data/lib/redd/auth_strategies/auth_strategy.rb +4 -3
- data/lib/redd/auth_strategies/script.rb +6 -1
- data/lib/redd/auth_strategies/userless.rb +6 -1
- data/lib/redd/auth_strategies/web.rb +4 -4
- data/lib/redd/models/access.rb +6 -1
- data/lib/redd/models/basic_model.rb +12 -55
- data/lib/redd/models/comment.rb +24 -28
- data/lib/redd/models/front_page.rb +1 -1
- data/lib/redd/models/gildable.rb +13 -0
- data/lib/redd/models/inboxable.rb +3 -3
- data/lib/redd/models/lazy_model.rb +7 -8
- data/lib/redd/models/listing.rb +6 -7
- data/lib/redd/models/live_thread.rb +9 -11
- data/lib/redd/models/mod_mail.rb +19 -24
- data/lib/redd/models/more_comments.rb +1 -1
- data/lib/redd/models/multireddit.rb +9 -13
- data/lib/redd/models/postable.rb +3 -3
- data/lib/redd/models/private_message.rb +18 -9
- data/lib/redd/models/searchable.rb +1 -1
- data/lib/redd/models/session.rb +31 -2
- data/lib/redd/models/submission.rb +26 -14
- data/lib/redd/models/subreddit.rb +115 -19
- data/lib/redd/models/user.rb +21 -9
- data/lib/redd/models/wiki_page.rb +8 -11
- data/lib/redd/utilities/unmarshaller.rb +3 -2
- data/lib/redd/version.rb +1 -1
- data/redd.gemspec +1 -1
- metadata +4 -5
- data/TODO.md +0 -8
- data/lib/redd/auth_strategies/installed.rb +0 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eb84c87afa0fb9151b0be21985da7f69c261df24
|
4
|
+
data.tar.gz: 2969272d1ae4cb25234ed9ff3902f2cdce51114d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ad8183da54e6dbbea67234b2cab3796b2aaf79b7bfd8f1e35dc7a6c3f93c5c92a0f63830c8d8f9d39042bcf02ed99841aea0bd4b225754a992328503464eea9
|
7
|
+
data.tar.gz: 8b7bce2305a66a554d484015e593d212f646aaead6efc894219e32ad14a2e86c4b99198d99fd01f83432938f1cf176c4fb2eaf7f1aae35112c5acf53b857c567
|
data/README.md
CHANGED
@@ -17,15 +17,22 @@
|
|
17
17
|
|
18
18
|
<!-- Intro Text -->
|
19
19
|
<p>
|
20
|
-
<strong>Redd</strong> is
|
21
|
-
for <a href="https://www.reddit.com/dev/api">reddit</a
|
22
|
-
that is all about being <strong>simple</strong>
|
23
|
-
and <strong>intuitive</strong>.
|
20
|
+
<strong>Redd</strong> is a <strong>batteries-included</strong>
|
21
|
+
API wrapper for <a href="https://www.reddit.com/dev/api">reddit</a>.
|
24
22
|
</p>
|
25
23
|
</div>
|
26
24
|
|
27
25
|
---
|
28
26
|
|
27
|
+
### Features
|
28
|
+
|
29
|
+
- Supports most of the reddit API, including live threads and the beta mod-mail.
|
30
|
+
- Includes support for streaming new posts and comments.
|
31
|
+
- Built-in rate limiting and error handling.
|
32
|
+
- Automatic retrying of failed requests.
|
33
|
+
|
34
|
+
### Demo
|
35
|
+
|
29
36
|
```ruby
|
30
37
|
require 'redd'
|
31
38
|
|
@@ -46,18 +53,21 @@ session.subreddit('all').comment_stream do |comment|
|
|
46
53
|
end
|
47
54
|
```
|
48
55
|
|
49
|
-
---
|
50
|
-
|
51
56
|
### FAQ
|
52
57
|
|
53
58
|
#### Is that bot fully functional?
|
54
|
-
**Yes**, that's all there is to it! You don't need to handle rate-limiting, refresh access tokens
|
55
|
-
or protect against issues on reddit's end (like 5xx errors).
|
59
|
+
**Yes**, that's all there is to it! You don't need to handle rate-limiting, refresh access tokens or protect against issues on reddit's end (like 5xx errors).
|
56
60
|
|
57
61
|
#### Where can I find the documentation?
|
58
62
|
|
59
63
|
[**Gem**](http://www.rubydoc.info/gems/redd/Redd/Models/Session) / [**GitHub**](http://www.rubydoc.info/github/avinashbot/redd/master/Redd/Models/Session)
|
60
64
|
|
65
|
+
#### How do I request a feature / contribute?
|
66
|
+
|
67
|
+
- The quickest way to get a feature into Redd is to raise a GitHub issue.
|
68
|
+
- Pull requests are also appreciated!
|
69
|
+
- Don't hesitate! There are no stupid questions!
|
70
|
+
|
61
71
|
#### How can I contact you?
|
62
72
|
[Reddit](https://www.reddit.com/message/compose/?to=Mustermind) /
|
63
73
|
[GitHub](https://github.com/avinashbot/redd/issues/new) /
|
data/bin/console
CHANGED
@@ -3,6 +3,14 @@
|
|
3
3
|
|
4
4
|
require 'webrick'
|
5
5
|
require 'redd'
|
6
|
+
require 'pry'
|
7
|
+
|
8
|
+
# The REPL session to initialize Pry in.
|
9
|
+
module Reddit
|
10
|
+
class << self
|
11
|
+
attr_accessor :reddit
|
12
|
+
end
|
13
|
+
end
|
6
14
|
|
7
15
|
server = WEBrick::HTTPServer.new(
|
8
16
|
Port: 8000,
|
@@ -22,7 +30,7 @@ server.mount_proc '/' do |_, res|
|
|
22
30
|
</style>
|
23
31
|
<div class="wrapper">
|
24
32
|
<h1>redd // quickstart</h1>
|
25
|
-
<a href="
|
33
|
+
<a href="/authenticate" target="_blank" id="btn">Start</a>
|
26
34
|
<span>a new session in your terminal?</span>
|
27
35
|
</div>
|
28
36
|
EOS
|
@@ -36,9 +44,10 @@ server.mount_proc '/authenticate' do |_, res|
|
|
36
44
|
response_type: 'code',
|
37
45
|
state: '0',
|
38
46
|
redirect_uri: 'http://localhost:8000/redirect',
|
39
|
-
'
|
40
|
-
|
41
|
-
|
47
|
+
duration: 'permanent',
|
48
|
+
scope: %w(account creddits edit flair history identity livemanage modconfig modcontributors
|
49
|
+
modflair modlog modmail modothers modposts modself modwiki mysubreddits
|
50
|
+
privatemessages read report save submit subscribe vote wikiedit wikiread)
|
42
51
|
)
|
43
52
|
)
|
44
53
|
end
|
@@ -53,11 +62,12 @@ server.mount_proc '/redirect' do |req, res|
|
|
53
62
|
EOS
|
54
63
|
|
55
64
|
unless err
|
56
|
-
|
65
|
+
Reddit.reddit = Redd.it(
|
57
66
|
user_agent: "Ruby:Redd-Quickstart:v#{Redd::VERSION} (by unknown)",
|
58
67
|
client_id: 'P4txR-D6TzF8cg',
|
59
68
|
redirect_uri: 'http://localhost:8000/redirect',
|
60
|
-
code: req.query['code']
|
69
|
+
code: req.query['code'],
|
70
|
+
auto_refresh: true
|
61
71
|
)
|
62
72
|
server.stop
|
63
73
|
end
|
@@ -72,14 +82,30 @@ rescue Interrupt
|
|
72
82
|
exit
|
73
83
|
end
|
74
84
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
85
|
+
Reddit.instance_exec do
|
86
|
+
# Post a colourful welcome message
|
87
|
+
suggestions = [
|
88
|
+
# Session#me
|
89
|
+
'reddit.me.link_karma',
|
90
|
+
# Subreddit listings
|
91
|
+
"reddit.subreddit('pics').hot.first.title",
|
92
|
+
# User listings
|
93
|
+
'puts reddit.me.comments(sort: :top).first.body',
|
94
|
+
# Sending messages
|
95
|
+
"reddit.user('Mustermind').send_message(subject: 'Hi!', text: 'How are you?')",
|
96
|
+
# Subscribing to subreddits
|
97
|
+
"reddit.subreddit('EarthPorn').subscribe",
|
98
|
+
# Upvoting
|
99
|
+
'reddit.front_page.hot(time: :month).first.upvote',
|
100
|
+
# Add friend
|
101
|
+
"reddit.user('Mustermind').friend",
|
102
|
+
# List friends
|
103
|
+
'reddit.friends.each { |friend| puts friend.name };',
|
104
|
+
# Hiding / Duplicates
|
105
|
+
'reddit.front_page.hot.each { |l| l.hide if l.duplicates.count > 2 }'
|
106
|
+
]
|
107
|
+
puts "Hi \e[35m/u/#{reddit.me.name}\e[0m! Try `\e[34m#{suggestions.sample}\e[0m`."
|
82
108
|
|
83
|
-
# Load Pry
|
84
|
-
|
85
|
-
|
109
|
+
# Load Pry
|
110
|
+
Pry.start(self)
|
111
|
+
end
|
data/lib/redd.rb
CHANGED
@@ -14,8 +14,40 @@ require_relative 'redd/api_client'
|
|
14
14
|
# Redd is a simple and intuitive API wrapper.
|
15
15
|
module Redd
|
16
16
|
class << self
|
17
|
-
#
|
18
|
-
#
|
17
|
+
# Based on the arguments you provide it, it guesses the appropriate authentication strategy.
|
18
|
+
# You can do this manually with:
|
19
|
+
#
|
20
|
+
# script = Redd::AuthStrategies::Script.new(**arguments)
|
21
|
+
# web = Redd::AuthStrategies::Web.new(**arguments)
|
22
|
+
# userless = Redd::AuthStrategies::Userless.new(**arguments)
|
23
|
+
#
|
24
|
+
# It then creates an {APIClient} with the auth strategy provided and calls authenticate on it:
|
25
|
+
#
|
26
|
+
# client = Redd::APIClient.new(script); client.authenticate(code)
|
27
|
+
# client = Redd::APIClient.new(web); client.authenticate
|
28
|
+
# client = Redd::APIClient.new(userless); client.authenticate
|
29
|
+
#
|
30
|
+
# Finally, it creates the {Models::Session} model, which is essentially a starting point for
|
31
|
+
# the user. But you can basically create any model with the client.
|
32
|
+
#
|
33
|
+
# session = Redd::Models::Session.new(client)
|
34
|
+
#
|
35
|
+
# user = Redd::Models::User.new(client, name: 'Mustermind')
|
36
|
+
# puts user.comment_karma
|
37
|
+
#
|
38
|
+
# If `auto_refresh` is `false` or if the access doesn't have an associated `expires_in`, you
|
39
|
+
# can manually refresh the token by calling:
|
40
|
+
#
|
41
|
+
# session.client.refresh
|
42
|
+
#
|
43
|
+
# Also, you can swap out the client's access any time.
|
44
|
+
#
|
45
|
+
# new_access = { access_token: '', refresh_token: '', expires_in: 1234 }
|
46
|
+
#
|
47
|
+
# session.client.access = Redd::Models::Access.new(script, new_access)
|
48
|
+
# session.client.access = Redd::Models::Access.new(web, new_access)
|
49
|
+
# session.client.access = Redd::Models::Access.new(userless, new_access)
|
50
|
+
#
|
19
51
|
# @see https://www.reddit.com/prefs/apps
|
20
52
|
# @param opts [Hash] the options to create the object with
|
21
53
|
# @option opts [String] :user_agent your app's *unique* and *descriptive* user agent
|
@@ -27,9 +59,9 @@ module Redd
|
|
27
59
|
# @option opts [String] :code the code given by reddit (required for *web* and *installed*)
|
28
60
|
# @return [Models::Session] a fresh {Models::Session} for you to make requests with
|
29
61
|
def it(opts = {})
|
30
|
-
api_client = script(opts) || web(opts) ||
|
62
|
+
api_client = script(opts) || web(opts) || userless(opts)
|
31
63
|
raise "couldn't guess app type" unless api_client
|
32
|
-
Models::Session.new(api_client)
|
64
|
+
Models::Session.new(api_client)
|
33
65
|
end
|
34
66
|
|
35
67
|
# Create a url to send to users for authorization.
|
@@ -38,14 +70,18 @@ module Redd
|
|
38
70
|
# @param client_id [String] the client id of the app
|
39
71
|
# @param redirect_uri [String] the URI for reddit to redirect to after authorization
|
40
72
|
# @param scope [Array<String>] an array of scopes to request
|
73
|
+
# @param duration ['temporary', 'permanent'] the duration to request the code for (only applies
|
74
|
+
# when response_type is 'code')
|
41
75
|
# @return [String] the generated url
|
42
|
-
def url(client_id:, redirect_uri:, response_type: 'code', state: '', scope: ['identity']
|
76
|
+
def url(client_id:, redirect_uri:, response_type: 'code', state: '', scope: ['identity'],
|
77
|
+
duration: 'temporary')
|
43
78
|
'https://www.reddit.com/api/v1/authorize?' + URI.encode_www_form(
|
44
79
|
client_id: client_id,
|
45
80
|
redirect_uri: redirect_uri,
|
46
81
|
state: state,
|
47
82
|
scope: scope.join(','),
|
48
|
-
response_type: response_type
|
83
|
+
response_type: response_type,
|
84
|
+
duration: duration
|
49
85
|
)
|
50
86
|
end
|
51
87
|
|
@@ -56,32 +92,27 @@ module Redd
|
|
56
92
|
end
|
57
93
|
|
58
94
|
def filter_api(opts)
|
59
|
-
opts.select { |k| %i(user_agent).include?(k) }
|
95
|
+
opts.select { |k| %i(user_agent limit_time max_retries auto_refresh).include?(k) }
|
60
96
|
end
|
61
97
|
|
62
98
|
def script(opts = {})
|
63
99
|
return unless %i(client_id secret username password).all? { |o| opts.include?(o) }
|
64
|
-
|
100
|
+
auth = AuthStrategies::Script.new(filter_auth(opts))
|
101
|
+
api = APIClient.new(auth, **filter_api(opts))
|
65
102
|
api.tap(&:authenticate)
|
66
103
|
end
|
67
104
|
|
68
105
|
def web(opts = {})
|
69
|
-
return unless %i(client_id secret redirect_uri code).all? { |o| opts.include?(o) }
|
70
|
-
code = opts.delete(:code)
|
71
|
-
api = APIClient.new(AuthStrategies::Web.new(**filter_auth(opts)), **filter_api(opts))
|
72
|
-
api.tap { |c| c.authenticate(code) }
|
73
|
-
end
|
74
|
-
|
75
|
-
def installed(opts = {})
|
76
106
|
return unless %i(client_id redirect_uri code).all? { |o| opts.include?(o) }
|
77
|
-
|
78
|
-
api = APIClient.new(
|
79
|
-
api.tap { |c| c.authenticate(code) }
|
107
|
+
auth = AuthStrategies::Web.new(**filter_auth(opts))
|
108
|
+
api = APIClient.new(auth, **filter_api(opts))
|
109
|
+
api.tap { |c| c.authenticate(opts[:code]) }
|
80
110
|
end
|
81
111
|
|
82
112
|
def userless(opts = {})
|
83
113
|
return unless %i(client_id secret).all? { |o| opts.include?(o) }
|
84
|
-
|
114
|
+
auth = AuthStrategies::Userless.new(filter_auth(opts))
|
115
|
+
api = APIClient.new(auth, **filter_api(opts))
|
85
116
|
api.tap(&:authenticate)
|
86
117
|
end
|
87
118
|
end
|
data/lib/redd/api_client.rb
CHANGED
@@ -13,35 +13,27 @@ module Redd
|
|
13
13
|
API_ENDPOINT = 'https://oauth.reddit.com'
|
14
14
|
|
15
15
|
# @return [APIClient] the access the client uses
|
16
|
-
|
16
|
+
attr_accessor :access
|
17
17
|
|
18
|
-
# Create a new API client
|
18
|
+
# Create a new API client with an auth strategy.
|
19
19
|
# @param auth [AuthStrategies::AuthStrategy] the auth strategy to use
|
20
20
|
# @param endpoint [String] the API endpoint
|
21
21
|
# @param user_agent [String] the user agent to send
|
22
22
|
# @param limit_time [Integer] the minimum number of seconds between each request
|
23
|
-
# @param max_retries [Integer] number of times to retry requests that may
|
24
|
-
#
|
25
|
-
# @param auto_login [Boolean] (for script and userless) automatically authenticate if not done
|
26
|
-
# so already
|
27
|
-
# @param auto_refresh [Boolean] (for script and userless) automatically refresh access token if
|
28
|
-
# nearing expiration
|
23
|
+
# @param max_retries [Integer] number of times to retry requests that may succeed if retried
|
24
|
+
# @param auto_refresh [Boolean] automatically refresh access token if nearing expiration
|
29
25
|
def initialize(auth, endpoint: API_ENDPOINT, user_agent: USER_AGENT, limit_time: 1,
|
30
|
-
max_retries: 5,
|
26
|
+
max_retries: 5, auto_refresh: true)
|
31
27
|
super(endpoint: endpoint, user_agent: user_agent)
|
32
28
|
|
33
|
-
@auth
|
34
|
-
@access
|
35
|
-
@max_retries
|
36
|
-
@failures
|
37
|
-
@error_handler
|
38
|
-
@rate_limiter
|
39
|
-
@unmarshaller
|
40
|
-
|
41
|
-
# FIXME: hard dependencies on Script and Userless types
|
42
|
-
can_auto = auth.is_a?(AuthStrategies::Script) || auth.is_a?(AuthStrategies::Userless)
|
43
|
-
@auto_login = can_auto && auto_login
|
44
|
-
@auto_refresh = can_auto && auto_refresh
|
29
|
+
@auth = auth
|
30
|
+
@access = nil
|
31
|
+
@max_retries = max_retries
|
32
|
+
@failures = 0
|
33
|
+
@error_handler = Utilities::ErrorHandler.new
|
34
|
+
@rate_limiter = Utilities::RateLimiter.new(limit_time)
|
35
|
+
@unmarshaller = Utilities::Unmarshaller.new(self)
|
36
|
+
@auto_refresh = auto_refresh
|
45
37
|
end
|
46
38
|
|
47
39
|
# Authenticate the client using the provided auth.
|
@@ -50,8 +42,8 @@ module Redd
|
|
50
42
|
end
|
51
43
|
|
52
44
|
# Refresh the access currently in use.
|
53
|
-
def refresh
|
54
|
-
@access = @auth.refresh(
|
45
|
+
def refresh
|
46
|
+
@access = @auth.refresh(@access)
|
55
47
|
end
|
56
48
|
|
57
49
|
# Revoke the current access and remove it from the client.
|
@@ -84,6 +76,7 @@ module Redd
|
|
84
76
|
@failures = 0
|
85
77
|
response
|
86
78
|
rescue Redd::ServerError, HTTP::TimeoutError => e
|
79
|
+
# FIXME: maybe only retry GET requests, for obvious reasons?
|
87
80
|
@failures += 1
|
88
81
|
raise e if @failures > @max_retries
|
89
82
|
warn "Redd got a #{e.class.name} error (#{e.message}), retrying..."
|
@@ -94,12 +87,10 @@ module Redd
|
|
94
87
|
|
95
88
|
# Makes sure a valid access is present, raising an error if nil
|
96
89
|
def ensure_access_is_valid
|
97
|
-
#
|
98
|
-
authenticate if @access.nil?
|
90
|
+
# If access is nil, panic
|
91
|
+
raise 'client access is nil, try calling #authenticate' if @access.nil?
|
99
92
|
# Refresh access if auto_refresh is enabled
|
100
93
|
refresh if @access.expired? && @auto_refresh
|
101
|
-
# Fuck it, panic
|
102
|
-
raise 'client access is nil, try calling #authenticate' if @access.nil?
|
103
94
|
end
|
104
95
|
|
105
96
|
def connection
|
@@ -26,13 +26,14 @@ module Redd
|
|
26
26
|
end
|
27
27
|
|
28
28
|
# @abstract Refresh the authentication and return the refreshed access
|
29
|
+
# @param _access [Access, String] the access to refresh
|
29
30
|
# @return [Access] the new access
|
30
|
-
def refresh(
|
31
|
+
def refresh(_access)
|
31
32
|
raise 'abstract method: this strategy cannot refresh access'
|
32
33
|
end
|
33
34
|
|
34
35
|
# Revoke the access token, making it invalid for future requests.
|
35
|
-
# @param access [Access] the access
|
36
|
+
# @param access [Access, String] the access to revoke
|
36
37
|
def revoke(access)
|
37
38
|
token =
|
38
39
|
if access.is_a?(String)
|
@@ -42,7 +43,7 @@ module Redd
|
|
42
43
|
else
|
43
44
|
access.access_token
|
44
45
|
end
|
45
|
-
post('/api/v1/revoke_token', token: token)
|
46
|
+
post('/api/v1/revoke_token', token: token)
|
46
47
|
end
|
47
48
|
|
48
49
|
private
|
@@ -17,7 +17,12 @@ module Redd
|
|
17
17
|
def authenticate
|
18
18
|
request_access('password', username: @username, password: @password)
|
19
19
|
end
|
20
|
-
|
20
|
+
|
21
|
+
# Refresh the authentication and return the refreshed access
|
22
|
+
# @return [Access] the new access
|
23
|
+
def refresh(_)
|
24
|
+
authenticate
|
25
|
+
end
|
21
26
|
end
|
22
27
|
end
|
23
28
|
end
|
@@ -11,7 +11,12 @@ module Redd
|
|
11
11
|
def authenticate
|
12
12
|
request_access('client_credentials')
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
|
+
# Refresh the authentication and return the refreshed access
|
16
|
+
# @return [Access] the new access
|
17
|
+
def refresh(_)
|
18
|
+
authenticate
|
19
|
+
end
|
15
20
|
end
|
16
21
|
end
|
17
22
|
end
|
@@ -4,10 +4,9 @@ require_relative 'auth_strategy'
|
|
4
4
|
|
5
5
|
module Redd
|
6
6
|
module AuthStrategies
|
7
|
-
# A typical code-based authentication
|
8
|
-
# Only confidential web apps can be refreshed.
|
7
|
+
# A typical code-based authentication, for 'web' and 'installed' types.
|
9
8
|
class Web < AuthStrategy
|
10
|
-
def initialize(client_id:,
|
9
|
+
def initialize(client_id:, redirect_uri:, secret: '', **kwargs)
|
11
10
|
super(client_id: client_id, secret: secret, **kwargs)
|
12
11
|
@redirect_uri = redirect_uri
|
13
12
|
end
|
@@ -22,7 +21,8 @@ module Redd
|
|
22
21
|
# Refresh the authentication and return a new refreshed access
|
23
22
|
# @return [Access] the new access
|
24
23
|
def refresh(access)
|
25
|
-
|
24
|
+
refresh_token = access.is_a?(String) ? refresh_token : access.refresh_token
|
25
|
+
request_access('refresh_token', refresh_token: refresh_token)
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
data/lib/redd/models/access.rb
CHANGED
@@ -7,7 +7,12 @@ module Redd
|
|
7
7
|
# Models access_token and related keys.
|
8
8
|
class Access < BasicModel
|
9
9
|
def expired?(grace_period = 60)
|
10
|
-
|
10
|
+
return false unless @attributes[:expires_in]
|
11
|
+
Time.now > @created_at + (@attributes[:expires_in] - grace_period)
|
12
|
+
end
|
13
|
+
|
14
|
+
def permanent?
|
15
|
+
!@attributes[:refresh_token].nil?
|
11
16
|
end
|
12
17
|
|
13
18
|
private
|