redd 0.8.1 → 0.8.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a0f001106f53af036bc79b0d02fb744f9f8807ac
4
- data.tar.gz: dea813c1bc65c8a42e7b533da29787e2f25d5786
3
+ metadata.gz: eb84c87afa0fb9151b0be21985da7f69c261df24
4
+ data.tar.gz: 2969272d1ae4cb25234ed9ff3902f2cdce51114d
5
5
  SHA512:
6
- metadata.gz: 67c4940373ced4e6513a03168e3e61e56353b5db665e53a5313987f7fd07d86b33f0e93d546428691ee077e9c7731da66a4f9d179858ddaca61b61bfbe6671f5
7
- data.tar.gz: 700cbec73a627b874ee3fd254c7cc2d78f8c0455f6c6c17574fa0a5518dd11b2438ff7e36be9e15aa01d2944c4d88bf63bc5a0b9cd811dcc2da775e1d7c00c70
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 an API wrapper
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="#" target="_blank" id="btn">Start</a>
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
- 'scope': %w(account creddits edit flair history identity livemanage modconfig modcontributors
40
- modflair modlog modmail modothers modposts modself modwiki mysubreddits
41
- privatemessages read report save submit subscribe vote wikiedit wikiread)
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
- R = Redd.it(
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
- # Post a colourful welcome message
76
- suggestions = [
77
- 'R.me.link_karma',
78
- "R.subreddit('pics').hot.first.title",
79
- "puts R.subreddit('all').hot.map(&:title)"
80
- ]
81
- puts "Welcome, \e[35m/u/#{R.me.name}\e[0m! Try `\e[34m#{suggestions.sample}\e[0m`."
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
- require 'pry'
85
- Pry.start
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
- # Guesses the appropriate authentication strategy, creates an API client and starts you off with
18
- # a {Models::Session}.
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) || installed(opts) || userless(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) { |client| client.get('/api/v1/me').body }
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
- api = APIClient.new(AuthStrategies::Script.new(**filter_auth(opts)), **filter_api(opts))
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
- code = opts.delete(:code)
78
- api = APIClient.new(AuthStrategies::Installed.new(**filter_auth(opts)), **filter_api(opts))
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
- api = APIClient.new(AuthStrategies::Userless.new(**filter_auth(opts)), **filter_api(opts))
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
@@ -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
- attr_reader :access
16
+ attr_accessor :access
17
17
 
18
- # Create a new API client from an auth strategy.
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 be successful if
24
- # retried
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, auto_login: true, auto_refresh: true)
26
+ max_retries: 5, auto_refresh: true)
31
27
  super(endpoint: endpoint, user_agent: user_agent)
32
28
 
33
- @auth = auth
34
- @access = nil
35
- @max_retries = max_retries
36
- @failures = 0
37
- @error_handler = Utilities::ErrorHandler.new
38
- @rate_limiter = Utilities::RateLimiter.new(limit_time)
39
- @unmarshaller = Utilities::Unmarshaller.new(self)
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(*args)
54
- @access = @auth.refresh(*args)
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
- # Authenticate first if auto_login is enabled
98
- authenticate if @access.nil? && @auto_login
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 object to revoke
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).body
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
- alias refresh authenticate
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
- alias refresh authenticate
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. I genuinely recommend this for bots.
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:, secret:, redirect_uri:, **kwargs)
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
- request_access('refresh_token', refresh_token: must_have(access, :refresh_token))
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
@@ -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
- Time.now > @created_at + (get_attribute(:expires_in) - grace_period)
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