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 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