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.
- checksums.yaml +7 -0
- data/.github/dependabot.yml +7 -0
- data/.github/workflows/ci.yml +52 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.rubocop.yml +29 -0
- data/CONTRIBUTING.md +63 -0
- data/Gemfile +6 -0
- data/Guardfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +119 -0
- data/Rakefile +12 -0
- data/TODO.md +423 -0
- data/bin/console +127 -0
- data/bin/guard +2 -0
- data/bin/setup +8 -0
- data/docs/guides/.keep +0 -0
- data/docs/tutorials/creating-bots-with-redd.md +101 -0
- data/docs/tutorials/creating-webapps-with-redd.md +124 -0
- data/docs/tutorials/make-a-grammar-bot.md +5 -0
- data/docs/tutorials.md +7 -0
- data/lib/redd/api_client.rb +116 -0
- data/lib/redd/assist/delete_badly_scoring.rb +64 -0
- data/lib/redd/auth_strategies/auth_strategy.rb +68 -0
- data/lib/redd/auth_strategies/script.rb +35 -0
- data/lib/redd/auth_strategies/userless.rb +29 -0
- data/lib/redd/auth_strategies/web.rb +36 -0
- data/lib/redd/client.rb +91 -0
- data/lib/redd/errors.rb +65 -0
- data/lib/redd/middleware.rb +125 -0
- data/lib/redd/models/access.rb +54 -0
- data/lib/redd/models/comment.rb +229 -0
- data/lib/redd/models/front_page.rb +55 -0
- data/lib/redd/models/gildable.rb +13 -0
- data/lib/redd/models/inboxable.rb +33 -0
- data/lib/redd/models/listing.rb +52 -0
- data/lib/redd/models/live_thread.rb +133 -0
- data/lib/redd/models/live_update.rb +46 -0
- data/lib/redd/models/messageable.rb +20 -0
- data/lib/redd/models/mod_action.rb +59 -0
- data/lib/redd/models/model.rb +23 -0
- data/lib/redd/models/moderatable.rb +46 -0
- data/lib/redd/models/modmail.rb +61 -0
- data/lib/redd/models/modmail_conversation.rb +154 -0
- data/lib/redd/models/modmail_message.rb +35 -0
- data/lib/redd/models/more_comments.rb +96 -0
- data/lib/redd/models/multireddit.rb +104 -0
- data/lib/redd/models/paginated_listing.rb +124 -0
- data/lib/redd/models/postable.rb +83 -0
- data/lib/redd/models/private_message.rb +105 -0
- data/lib/redd/models/replyable.rb +16 -0
- data/lib/redd/models/reportable.rb +14 -0
- data/lib/redd/models/searchable.rb +35 -0
- data/lib/redd/models/self.rb +17 -0
- data/lib/redd/models/session.rb +198 -0
- data/lib/redd/models/submission.rb +405 -0
- data/lib/redd/models/subreddit.rb +670 -0
- data/lib/redd/models/trophy.rb +34 -0
- data/lib/redd/models/user.rb +239 -0
- data/lib/redd/models/wiki_page.rb +56 -0
- data/lib/redd/utilities/error_handler.rb +73 -0
- data/lib/redd/utilities/rate_limiter.rb +21 -0
- data/lib/redd/utilities/unmarshaller.rb +70 -0
- data/lib/redd/version.rb +5 -0
- data/lib/redd.rb +129 -0
- data/lib/spinels-redd.rb +3 -0
- data/logo.png +0 -0
- data/spinels-redd.gemspec +39 -0
- 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.
|
data/docs/tutorials.md
ADDED
@@ -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
|