spinels-redd 0.9.0

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.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.github/dependabot.yml +7 -0
  3. data/.github/workflows/ci.yml +52 -0
  4. data/.gitignore +10 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +29 -0
  7. data/CONTRIBUTING.md +63 -0
  8. data/Gemfile +6 -0
  9. data/Guardfile +7 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +119 -0
  12. data/Rakefile +12 -0
  13. data/TODO.md +423 -0
  14. data/bin/console +127 -0
  15. data/bin/guard +2 -0
  16. data/bin/setup +8 -0
  17. data/docs/guides/.keep +0 -0
  18. data/docs/tutorials/creating-bots-with-redd.md +101 -0
  19. data/docs/tutorials/creating-webapps-with-redd.md +124 -0
  20. data/docs/tutorials/make-a-grammar-bot.md +5 -0
  21. data/docs/tutorials.md +7 -0
  22. data/lib/redd/api_client.rb +116 -0
  23. data/lib/redd/assist/delete_badly_scoring.rb +64 -0
  24. data/lib/redd/auth_strategies/auth_strategy.rb +68 -0
  25. data/lib/redd/auth_strategies/script.rb +35 -0
  26. data/lib/redd/auth_strategies/userless.rb +29 -0
  27. data/lib/redd/auth_strategies/web.rb +36 -0
  28. data/lib/redd/client.rb +91 -0
  29. data/lib/redd/errors.rb +65 -0
  30. data/lib/redd/middleware.rb +125 -0
  31. data/lib/redd/models/access.rb +54 -0
  32. data/lib/redd/models/comment.rb +229 -0
  33. data/lib/redd/models/front_page.rb +55 -0
  34. data/lib/redd/models/gildable.rb +13 -0
  35. data/lib/redd/models/inboxable.rb +33 -0
  36. data/lib/redd/models/listing.rb +52 -0
  37. data/lib/redd/models/live_thread.rb +133 -0
  38. data/lib/redd/models/live_update.rb +46 -0
  39. data/lib/redd/models/messageable.rb +20 -0
  40. data/lib/redd/models/mod_action.rb +59 -0
  41. data/lib/redd/models/model.rb +23 -0
  42. data/lib/redd/models/moderatable.rb +46 -0
  43. data/lib/redd/models/modmail.rb +61 -0
  44. data/lib/redd/models/modmail_conversation.rb +154 -0
  45. data/lib/redd/models/modmail_message.rb +35 -0
  46. data/lib/redd/models/more_comments.rb +96 -0
  47. data/lib/redd/models/multireddit.rb +104 -0
  48. data/lib/redd/models/paginated_listing.rb +124 -0
  49. data/lib/redd/models/postable.rb +83 -0
  50. data/lib/redd/models/private_message.rb +105 -0
  51. data/lib/redd/models/replyable.rb +16 -0
  52. data/lib/redd/models/reportable.rb +14 -0
  53. data/lib/redd/models/searchable.rb +35 -0
  54. data/lib/redd/models/self.rb +17 -0
  55. data/lib/redd/models/session.rb +198 -0
  56. data/lib/redd/models/submission.rb +405 -0
  57. data/lib/redd/models/subreddit.rb +670 -0
  58. data/lib/redd/models/trophy.rb +34 -0
  59. data/lib/redd/models/user.rb +239 -0
  60. data/lib/redd/models/wiki_page.rb +56 -0
  61. data/lib/redd/utilities/error_handler.rb +73 -0
  62. data/lib/redd/utilities/rate_limiter.rb +21 -0
  63. data/lib/redd/utilities/unmarshaller.rb +70 -0
  64. data/lib/redd/version.rb +5 -0
  65. data/lib/redd.rb +129 -0
  66. data/lib/spinels-redd.rb +3 -0
  67. data/logo.png +0 -0
  68. data/spinels-redd.gemspec +39 -0
  69. metadata +298 -0
@@ -0,0 +1,101 @@
1
+ ---
2
+ title: Creating Bots with Redd
3
+ path: /tutorials/creating-bots-with-redd
4
+ ---
5
+
6
+ ## Step 1: Creating an Application
7
+
8
+ Every bot needs to be registered on reddit. Some API wrappers let you skip this, but chances are, you will be heavily rate limited. Start by visiting your [**applications**](https://www.reddit.com/prefs/apps#developed-apps) and skipping to the "**developed apps**" section and clicking the button to create an app. Give it a name and a description and select the **script** app type. If you want to create a web application, [**follow this guide**](/tutorials/creating-webapps-with-redd). The text input labeled "redirect uri" is mandatory but you can just put any url in there because it doesn't really matter.
9
+
10
+ ## Step 2: Creating a Bot Account (optional)
11
+
12
+ If you want your bot to interact with other users on the site (like submitting posts or replying to comments), you'll need to create a bot account. Log out and click the "Log in or sign up" link at the top right. Pick a nice bot name and a secure password (a valid email is encouraged in case you forget the password).
13
+
14
+ **This step is important!** Go to the application [you just created](https://www.reddit.com/prefs/apps#developed-apps) and add the bot's username as a developer for the app.
15
+
16
+ ## Step 3: Installing Redd
17
+
18
+ Go ahead and [install Ruby](https://www.ruby-lang.org/en/downloads/) if you haven't already. Most package managers have a pretty recent version of Ruby up. The latest version at the time of writing is `2.4.1`. Redd supports Ruby versions `2.1.0` and up, but it's recommended to get the latest version, since I'll probably stop supporting older versions as time passes.
19
+
20
+ While you can just run `gem install redd` and call it a day, we're going to use [Bundler](https://bundler.io) like the responsible Rubyist we are.
21
+
22
+ 1. Let's make sure bundler is installed:
23
+
24
+ $ gem install bundler
25
+
26
+ 2. Then, create a folder for your bot's code to live in:
27
+
28
+ $ mkdir myfancybot
29
+ $ cd myfancybot
30
+
31
+ 3. Create a `Gemfile`listing all the gems our bot is going to use. It should look something like this:
32
+
33
+ ```ruby
34
+ source 'https://rubygems.org'
35
+ gem 'redd', '~> 0.8'
36
+ ```
37
+
38
+ 4. Okay, now we just need to download all the gems and then we're ready to get coding!
39
+
40
+ $ bundle install
41
+
42
+ ## Step 4: Test Drive
43
+
44
+ Let's create the file our bot is going to run from. I'm going to call it `app.rb`, but feel free to use your imagination. Let's put the following in it to start off:
45
+
46
+ ```ruby
47
+ require 'bundler/setup'
48
+ require 'redd'
49
+ ```
50
+ Redd has a pretty convenient method to get us up and running with the API as quickly as possible called [`Redd.it`](http://www.rubydoc.info/github/avinashbot/redd/master/Redd#it-class_method). I'm going to use that to plug in all my application details. Add the following to your bot's file, replacing the text in angled brackets with your app's details. If you don't need an account for your bot, just skip the `username` and `password` fields.
51
+
52
+ ```ruby
53
+ reddit = Redd.it(
54
+ user_agent: 'Redd:MyFancyBot:v1.2.3',
55
+ client_id: '[the code under the title of your app]',
56
+ secret: '[the apps secret]',
57
+ username: '[your bot account username]',
58
+ password: '[your bot account password]'
59
+ )
60
+ ```
61
+
62
+ For more info on the user_agent, take a look at reddit's [API rules](https://github.com/reddit/reddit/wiki/API).
63
+
64
+ Now if everything went as planned, `reddit` should be a valid reddit session. The [**Session documentation**](http://www.rubydoc.info/github/avinashbot/redd/master/Redd/Models/Session) lists all the things you can do from here. But for now, let's just print out the title of the post on top of /r/all right now by adding the following to your code:
65
+
66
+ ```ruby
67
+ r_all = reddit.subreddit('all')
68
+ post = r_all.hot.first
69
+ puts post.title
70
+ ```
71
+
72
+ **We're all done!** Let's run this baby!
73
+
74
+ $ ruby app.rb
75
+
76
+ ## Step 6: Making your Dream Bot
77
+
78
+ The [**documentation**](http://www.rubydoc.info/github/avinashbot/redd/master/Redd/Models/Session) is a great starting point for things you can do with Redd. If you need help with Ruby, /r/learnruby is a great place to visit. If you need help with the Reddit API, /r/redditdev is also very helpful, along with [reddit's API documentation](https://reddit.com/dev/api).
79
+
80
+ **Remember to respect the the [bottiquette](https://www.reddit.com/wiki/bottiquette)!** P.S. /r/Bottiquette keeps a [list of subreddits where bots aren't allowed](https://www.reddit.com/r/Bottiquette/wiki/robots_txt), while [/r/botsrights does the opposite](https://www.reddit.com/r/botsrights/wiki/listofbotfriendlysubs).
81
+
82
+ ## Step 7: Making it Legendary
83
+
84
+ While it convenient that you can run the bot straight from your home, running a bot 24/7 from your overheating laptop isn't exactly the best long term solution, ya dig? There are three solutions to this dilemma:
85
+
86
+ 1. Abuse free pricing tiers:
87
+ - [**Heroku**](https://www.heroku.com/free) offers up to 1000 dyno hours per account per month.
88
+ - [**AWS**](https://aws.amazon.com/free/) has a free tier, offering 750 hours a month for a year.
89
+ - [**Google Cloud Platform**](https://cloud.google.com/free/docs/always-free-usage-limits) has an always free tier, offering 28 instance hours a day.
90
+
91
+ 2. Five bucks a month:
92
+ Both [DigitalOcean](https://www.digitalocean.com/pricing) and [Linode](https://www.linode.com/pricing) offer cloud computing for $5 a month.
93
+
94
+ 3. One-time payment:
95
+ Buy a [Raspberry Pi](https://www.raspberrypi.org/products/) or a [CHIP](https://getchip.com/) to run from your home.
96
+
97
+ ---
98
+
99
+ I hope that got you started making your first reddit bot! If you need help, don't hesitate to [**submit a self post**](https://www.reddit.com/r/Redd/submit?selftext=true) or [**PM me**](https://www.reddit.com/message/compose/?to=Mustermind).
100
+
101
+ If you're uploading your Ruby bot to GitHub, post a link to the bot's code to the subreddit!
@@ -0,0 +1,124 @@
1
+ ---
2
+ title: Creating Webapps with Redd
3
+ path: /tutorials/creating-webapps-with-redd
4
+ ---
5
+ Redd can also help with simplifying web apps that need to use the reddit API thanks to OAuth2. This guide assumes you are somewhat familiar with web development on Ruby and skips some of the basic steps (you can always ask if you need help). The full code from this example can be found [**here**](https://gist.github.com/avinashbot/f298efca5622d77e4ba65abc57c253e4).
6
+
7
+ ## A quick primer on OAuth2
8
+
9
+ 1. A user goes to our fancy website.
10
+ 2. Then, they see a cool orangered button labeled "login with reddit" and click it.
11
+ 3. Our website then **redirects them to reddit**, which asks the user if they are sure want to login through reddit.
12
+ 4. If they change their mind and press "Decline", **reddit redirects the user back to our site** saying that they denied giving us access.
13
+ 5. If they press "Allow", **reddit redirects the user back to our site with a code**.
14
+ 6. We can now **make a request to reddit with the code to get an access token** (and maybe a refresh token too).
15
+ 7. Tada! Now that we have an access token, we can make API requests to reddit.
16
+ 8. Once the access token has **expired**, we ask reddit to give us a new one using our refresh token.
17
+
18
+ ## Step 1: Creating an Application
19
+
20
+ Start by visiting your [**applications**](https://www.reddit.com/prefs/apps#developed-apps) and skipping to the "**developed apps**" section and clicking the button to create an app. Give it a name and a description. Make it friendly, because the user is going to see them! You are also given a choice between three app types (two of which are relevant to us).
21
+
22
+ - **web app**: if you're running the application from a trusted server
23
+ - **installed app**: if you're running the application on the user's device
24
+
25
+ The "*redirect uri*" is the URL that the user gets redirected to once they've either allowed or denied access to reddit.
26
+
27
+ ## Step 2: Break out the Rails (or Sinatra)
28
+
29
+ Let's start with a simple Sinatra app.
30
+
31
+ ```ruby
32
+ require 'bundler/setup'
33
+ require 'sinatra'
34
+ require 'redd/middleware'
35
+
36
+ enable :sessions
37
+
38
+ get '/' do
39
+ '<a href="/login">login with reddit</a>'
40
+ end
41
+ ```
42
+
43
+ Sweet. That login link will 404, but we're going to fix that in next step.
44
+
45
+ ## Step 3: The Secret Weapon
46
+
47
+ Redd (from `v0.8.6` onwards) sports a new [Rack Middleware](http://www.rubydoc.info/github/avinashbot/redd/master/Redd/Middleware) that makes integrating Redd with traditional web-apps much easier. In your preferred framework (guides for [Rails](http://guides.rubyonrails.org/rails_on_rack.html#adding-a-middleware), [Sinatra](http://www.sinatrarb.com/intro.html#Rack%20Middleware), [Rackup](https://github.com/rack/rack/wiki/%28tutorial%29-rackup-howto)), add the Redd::Middleware, providing it options similar to the following:
48
+
49
+ ```ruby
50
+ use Redd::Middleware,
51
+ user_agent: 'Redd:Your App:v1.0.0 (by /u/<your username>)',
52
+ client_id: '<your client id>',
53
+ secret: '<your app secret>',
54
+ redirect_uri: '<your redirect uri>',
55
+ scope: %w(identity),
56
+ via: '/login'
57
+ ```
58
+
59
+ Before continuing, **make sure that sessions are enabled**.
60
+
61
+ - Rails: has it in by default. No work needed on your end.
62
+ - Sinatra: Add `enable :sessions` **before** adding the `Redd::Middleware`.
63
+ - Rackup: Add `use Rack::Session::Cookie` **before** adding the `Redd::Middleware`.
64
+
65
+ Modify as needed. The `via` option specifies the path that takes the user to the authorization page (`/auth/reddit` by default). We'll use `/login` since that's we used in the previous step.
66
+
67
+ ## Step 4: All Logged In (Maybe)
68
+
69
+ When the user comes back to our site, they are sent to the redirect uri that we gave reddit. So let's define a route so that our user isn't met with a 404 when they authorize us. Assuming our redirect uri is something like `https://<your website>/redirect`, the code should look like this:
70
+
71
+ ```ruby
72
+ get '/redirect' do
73
+ redirect to('/profile')
74
+ end
75
+ ```
76
+
77
+ But what if the user clicked deny? Or what if the user got sent to us because of a [CSRF attack](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_%28CSRF%29)? Redd detects that and puts an exception in the 'redd.error' rack environment variable. If you already have a mechanism in place to deal with exceptions, you can just raise the error.
78
+
79
+ ```ruby
80
+ get '/redirect' do
81
+ redirect to('/profile') if request.env['redd.error'].nil?
82
+
83
+ if request.env['redd.error'].message == 'access_denied'
84
+ 'Sorry, you clicked decline. <a href="/login">Login again?</a>'
85
+ elsif request.env['redd.error'].message == 'invalid_state'
86
+ 'Did you login through our website? <a href="/login">(No)</a>'
87
+ else
88
+ puts "Error while logging in!"
89
+ raise request.env['redd.error'] # Raise a 500 and make a mental note to look at the logs later
90
+ end
91
+ end
92
+ ```
93
+
94
+ Much better.
95
+
96
+ ## Step 5: Using the API
97
+
98
+ ```ruby
99
+ get '/profile' do
100
+ "Hi, #{request.env['redd.session'].me.name}! <a href='/logout'>Log out</a>"
101
+ end
102
+ ```
103
+
104
+ Well, that was easy! `redd.session` is a [**Session**](http://www.rubydoc.info/github/avinashbot/redd/master/Redd/Models/Session) model, which is basically a starting point for all the API calls that Redd offers.
105
+
106
+ ## Step 6: Logging out
107
+
108
+ ```ruby
109
+ get '/logout' do
110
+ request.env['redd.session'] = nil
111
+ redirect to('/')
112
+ end
113
+ ```
114
+
115
+ Well, there isn't anything I need to explain, is there?
116
+
117
+ ---
118
+
119
+ I know you're just excited to build your awesome website, but before you leave, a few tips:
120
+
121
+ - Here's the [**documentation**](http://www.rubydoc.info/github/avinashbot/redd/master/Redd/Models/Session), and feel free to [**shoot me a message**](https://www.reddit.com/message/compose/?to=Mustermind) if you need help.
122
+ - Rate limits still apply (although they're per user). If you're going to be making many API requests per page request, you might want to look into caching.
123
+ - There are restrictions on commercial use of the API. [**Here's the rules on that**](https://www.reddit.com/wiki/api).
124
+ - I love hearing about the crazy ways you people make use of my gem! Feel free to post in this subreddit if you have an application that relies on Redd.
@@ -0,0 +1,5 @@
1
+ ---
2
+ title: Make a Grammar Bot
3
+ ---
4
+
5
+ TODO: add more content
data/docs/tutorials.md ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ title: Tutorials
3
+ ---
4
+
5
+ - [Creating Bots with Redd](/tutorials/creating-bots-with-redd)
6
+ - [Creating Webapps with Redd](/tutorials/creating-webapps-with-redd)
7
+ - [Make a Grammar Bot (_in progress_)](/tutorials/make-a-grammar-bot)
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'client'
4
+ require_relative 'utilities/error_handler'
5
+ require_relative 'utilities/rate_limiter'
6
+ require_relative 'utilities/unmarshaller'
7
+
8
+ module Redd
9
+ # The class for API clients.
10
+ class APIClient < Client
11
+ # The endpoint to make API requests to.
12
+ API_ENDPOINT = 'https://oauth.reddit.com'
13
+
14
+ # @return [APIClient] the access the client uses
15
+ attr_accessor :access
16
+
17
+ # Create a new API client with an auth strategy.
18
+ # TODO: Give user option to pass through all retryable errors.
19
+ # @param auth [AuthStrategies::AuthStrategy] the auth strategy to use
20
+ # @param endpoint [String] the API endpoint
21
+ # @param user_agent [String] the user agent to send
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 succeed if retried
24
+ # @param auto_refresh [Boolean] automatically refresh access token if nearing expiration
25
+ def initialize(auth, endpoint: API_ENDPOINT, user_agent: USER_AGENT, limit_time: 1,
26
+ max_retries: 5, auto_refresh: true)
27
+ super(endpoint: endpoint, user_agent: user_agent)
28
+
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
37
+ end
38
+
39
+ # Authenticate the client using the provided auth.
40
+ def authenticate(*args)
41
+ @access = @auth.authenticate(*args)
42
+ end
43
+
44
+ # Refresh the access currently in use.
45
+ def refresh
46
+ @access = @auth.refresh(@access)
47
+ end
48
+
49
+ # Revoke the current access and remove it from the client.
50
+ def revoke
51
+ @auth.revoke(@access)
52
+ @access = nil
53
+ end
54
+
55
+ def unmarshal(object)
56
+ @unmarshaller.unmarshal(object)
57
+ end
58
+
59
+ def model(verb, path, options = {})
60
+ unmarshal(send(verb, path, options).body)
61
+ end
62
+
63
+ # Makes a request, ensuring not to break the rate limit by sleeping.
64
+ # @see Client#request
65
+ def request(verb, path, raw: false, params: {}, **options)
66
+ # Make sure @access is populated by a valid access
67
+ ensure_access_is_valid
68
+ # Setup base API params and make request
69
+ api_params = { api_type: 'json', raw_json: 1 }.merge(params)
70
+
71
+ # This loop is retried @max_retries number of times until it succeeds
72
+ handle_retryable_errors do
73
+ response = @rate_limiter.after_limit { super(verb, path, params: api_params, **options) }
74
+ # Raise errors if encountered at the API level.
75
+ response_error = @error_handler.check_error(response, raw: raw)
76
+ raise response_error unless response_error.nil?
77
+
78
+ # All done, return the response
79
+ response
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ # Makes sure a valid access is present, raising an error if nil
86
+ def ensure_access_is_valid
87
+ # If access is nil, panic
88
+ raise 'client access is nil, try calling #authenticate' if @access.nil?
89
+
90
+ # Refresh access if auto_refresh is enabled
91
+ refresh if @access.expired? && @auto_refresh && @auth && @auth.refreshable?(@access)
92
+ end
93
+
94
+ def handle_retryable_errors # rubocop:disable Metrics/MethodLength
95
+ response = yield
96
+ rescue Errors::ServerError, HTTP::TimeoutError => e
97
+ # FIXME: maybe only retry GET requests, for obvious reasons?
98
+ @failures += 1
99
+ raise e if @failures > @max_retries
100
+
101
+ warn "Redd got a #{e.class.name} error (#{e.message}), retrying..."
102
+ retry
103
+ rescue Errors::RateLimitError => e
104
+ warn "Redd was rate limited for #{e.duration} seconds, waiting..."
105
+ sleep e.duration
106
+ retry
107
+ else
108
+ @failures = 0
109
+ response
110
+ end
111
+
112
+ def connection
113
+ super.auth("Bearer #{@access.access_token}")
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Redd
4
+ module Assist
5
+ # Helper class for deleting negative links or comments. You can run this as a separate process,
6
+ # as long as you set your rate limit time higher in your main process.
7
+ # @note Works with {Submission} and {Comment}.
8
+ # @example
9
+ # assist = Redd::Assist::DeleteBadlyScoring.new(session.client)
10
+ # assist.track(comment)
11
+ # loop do
12
+ # assist.delete_badly_scoring(under_score: 1, minimum_age: 600)
13
+ # sleep 30
14
+ # end
15
+ class DeleteBadlyScoring
16
+ # Create a DeleteBadlyScoring assist.
17
+ # @param client [APIClient] the API client
18
+ def initialize(client)
19
+ @client = client
20
+ @queue = []
21
+ end
22
+
23
+ # Add this model's id to the list of items that are tracked.
24
+ def track(model)
25
+ @queue << model.name
26
+ end
27
+
28
+ # Delete all items that are older than the minimum age and score 0 or below.
29
+ # @param under_score [Integer] the maximum score that the comment must have to be kept
30
+ # @param minimum_age [Integer] the minimum age for deletion (seconds)
31
+ # @return [Array<String>] the deleted item fullnames
32
+ def delete_badly_scoring!(under_score: 1, minimum_age: 15 * 60)
33
+ delete_if do |comment|
34
+ if comment.created_at + minimum_age > Time.now
35
+ :skip
36
+ elsif comment.score < under_score && !comment.deleted? && !comment.archived?
37
+ :delete
38
+ else
39
+ :keep
40
+ end
41
+ end
42
+ end
43
+
44
+ # Delete all items that the block returns true for.
45
+ # @param minimum_age [Integer] the minimum age for deletion
46
+ # @yieldparam comment [Comment] the comment to filter
47
+ # @yieldreturn [:keep, :delete, :skip] whether to keep, delete, or check again later
48
+ # @return [Array<String>] the deleted item fullnames
49
+ def delete_if # rubocop:disable Metrics/MethodLength
50
+ deleted = []
51
+ @queue.delete_if do |fullname|
52
+ comment = Models::Comment.new(@client, name: fullname).reload
53
+ action = yield comment
54
+ if action == :delete
55
+ comment.delete
56
+ deleted << fullname
57
+ end
58
+ %i[keep delete].include?(action)
59
+ end
60
+ deleted
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../client'
4
+
5
+ module Redd
6
+ module AuthStrategies
7
+ # The API client for authentication to reddit.
8
+ class AuthStrategy < Client
9
+ # The API to make authentication requests to.
10
+ AUTH_ENDPOINT = 'https://www.reddit.com'
11
+
12
+ # @param client_id [String] the client id of the reddit app
13
+ # @param secret [String] the app's secret string
14
+ # @param endpoint [String] the url to contact for authentication requests
15
+ # @param user_agent [String] the user agent to send with requests
16
+ def initialize(client_id:, secret:, endpoint: AUTH_ENDPOINT, user_agent: USER_AGENT)
17
+ super(endpoint: endpoint, user_agent: user_agent)
18
+ @client_id = client_id
19
+ @secret = secret
20
+ end
21
+
22
+ # @abstract Perform authentication and return the resulting access object
23
+ # @return [Access] the access token object
24
+ def authenticate(*)
25
+ raise 'abstract method: this strategy cannot authenticate with reddit'
26
+ end
27
+
28
+ # @return [Boolean] whether the access object can be refreshed
29
+ def refreshable?(_access)
30
+ false
31
+ end
32
+
33
+ # @abstract Refresh the authentication and return the refreshed access
34
+ # @param _access [Access, String] the access to refresh
35
+ # @return [Access] the new access
36
+ def refresh(_access)
37
+ raise 'abstract method: this strategy cannot refresh access'
38
+ end
39
+
40
+ # Revoke the access token, making it invalid for future requests.
41
+ # @param access [Access, String] the access to revoke
42
+ def revoke(access)
43
+ token =
44
+ if access.is_a?(String)
45
+ access
46
+ elsif access.respond_to?(:refresh_token)
47
+ access.refresh_token
48
+ else
49
+ access.access_token
50
+ end
51
+ post('/api/v1/revoke_token', token: token)
52
+ end
53
+
54
+ private
55
+
56
+ def connection
57
+ @connection ||= super.basic_auth(user: @client_id, pass: @secret)
58
+ end
59
+
60
+ def request_access(grant_type, options = {})
61
+ response = post('/api/v1/access_token', { grant_type: grant_type }.merge(options))
62
+ raise Errors::AuthenticationError.new(response) if response.body.key?(:error)
63
+
64
+ Models::Access.new(response.body)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'auth_strategy'
4
+
5
+ module Redd
6
+ module AuthStrategies
7
+ # A password-based authentication scheme. Requests all scopes.
8
+ class Script < AuthStrategy
9
+ def initialize(client_id:, secret:, username:, password:)
10
+ super(client_id: client_id, secret: secret)
11
+ @username = username
12
+ @password = password
13
+ end
14
+
15
+ # Perform authentication and return the resulting access object
16
+ # @return [Access] the access token object
17
+ def authenticate
18
+ request_access('password', username: @username, password: @password)
19
+ end
20
+
21
+ # Since the access isn't used for refreshing, the strategy is inherently
22
+ # refreshable.
23
+ # @return [true]
24
+ def refreshable?(_access)
25
+ true
26
+ end
27
+
28
+ # Refresh the authentication and return the refreshed access
29
+ # @return [Access] the new access
30
+ def refresh(_)
31
+ authenticate
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'auth_strategy'
4
+
5
+ module Redd
6
+ module AuthStrategies
7
+ # A userless authentication scheme.
8
+ class Userless < AuthStrategy
9
+ # Perform authentication and return the resulting access object
10
+ # @return [Access] the access token object
11
+ def authenticate
12
+ request_access('client_credentials')
13
+ end
14
+
15
+ # Since the access isn't used for refreshing, the strategy is inherently
16
+ # refreshable.
17
+ # @return [true]
18
+ def refreshable?(_access)
19
+ true
20
+ end
21
+
22
+ # Refresh the authentication and return the refreshed access
23
+ # @return [Access] the new access
24
+ def refresh(_)
25
+ authenticate
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'auth_strategy'
4
+
5
+ module Redd
6
+ module AuthStrategies
7
+ # A typical code-based authentication, for 'web' and 'installed' types.
8
+ class Web < AuthStrategy
9
+ def initialize(client_id:, redirect_uri:, secret: '', **kwargs)
10
+ super(client_id: client_id, secret: secret, **kwargs)
11
+ @redirect_uri = redirect_uri
12
+ end
13
+
14
+ # Authenticate with a code using the "web" flow.
15
+ # @param code [String] the code returned by reddit
16
+ # @return [Access]
17
+ def authenticate(code)
18
+ request_access('authorization_code', code: code, redirect_uri: @redirect_uri)
19
+ end
20
+
21
+ # @return [Boolean] whether the access has a refresh token
22
+ def refreshable?(access)
23
+ access.permanent?
24
+ end
25
+
26
+ # Refresh the authentication and return a new refreshed access
27
+ # @return [Access] the new access
28
+ def refresh(access)
29
+ token = access.is_a?(String) ? access : access.refresh_token
30
+ response = post('/api/v1/access_token', grant_type: 'refresh_token', refresh_token: token)
31
+ # When refreshed, the response doesn't include an access token, so we have to add it.
32
+ Models::Access.new(response.body.merge(refresh_token: token))
33
+ end
34
+ end
35
+ end
36
+ end