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
data/lib/redd/client.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'http'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Redd
|
7
|
+
# The base class for JSON-based HTTP clients. Generic enough to be used for basically anything.
|
8
|
+
class Client
|
9
|
+
# The default User-Agent to use if none was provided.
|
10
|
+
USER_AGENT = "Ruby:Redd:v#{Redd::VERSION} (by unknown)"
|
11
|
+
|
12
|
+
# Holds a returned HTTP response.
|
13
|
+
Response = Struct.new(:code, :headers, :raw_body) do
|
14
|
+
def body
|
15
|
+
@body ||= JSON.parse(raw_body, symbolize_names: true)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Create a new client.
|
20
|
+
# @param endpoint [String] the base endpoint to make all requests from
|
21
|
+
# @param user_agent [String] a user agent string
|
22
|
+
def initialize(endpoint:, user_agent: USER_AGENT)
|
23
|
+
@endpoint = endpoint
|
24
|
+
@user_agent = user_agent
|
25
|
+
end
|
26
|
+
|
27
|
+
# Make an HTTP request.
|
28
|
+
# @param verb [:get, :post, :put, :patch, :delete] the HTTP verb to use
|
29
|
+
# @param path [String] the path relative to the endpoint
|
30
|
+
# @param options [Hash] the request parameters
|
31
|
+
# @option options [Hash] :params the parameters to supply with the url
|
32
|
+
# @option options [Hash] :form the parameters to supply in the body
|
33
|
+
# @option options [Hash] :body the direct body contents
|
34
|
+
# @return [Response] the response
|
35
|
+
def request(verb, path, options = {})
|
36
|
+
# puts "#{verb.to_s.upcase} #{path}", ' ' + options.inspect
|
37
|
+
response = connection.request(verb, path, **options)
|
38
|
+
Response.new(response.status.code, response.headers, response.body.to_s)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Make a GET request.
|
42
|
+
# @param path [String] the path relative to the endpoint
|
43
|
+
# @param options [Hash] the parameters to supply
|
44
|
+
# @return [Response] the response
|
45
|
+
def get(path, options = {})
|
46
|
+
request(:get, path, params: options)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Make a POST request.
|
50
|
+
# @param path [String] the path relative to the endpoint
|
51
|
+
# @param options [Hash] the parameters to supply
|
52
|
+
# @return [Response] the response
|
53
|
+
def post(path, options = {})
|
54
|
+
request(:post, path, form: options)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Make a PUT request.
|
58
|
+
# @param path [String] the path relative to the endpoint
|
59
|
+
# @param options [Hash] the parameters to supply
|
60
|
+
# @return [Response] the response
|
61
|
+
def put(path, options = {})
|
62
|
+
request(:put, path, form: options)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Make a PATCH request.
|
66
|
+
# @param path [String] the path relative to the endpoint
|
67
|
+
# @param options [Hash] the parameters to supply
|
68
|
+
# @return [Response] the response
|
69
|
+
def patch(path, options = {})
|
70
|
+
request(:patch, path, form: options)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Make a DELETE request.
|
74
|
+
# @param path [String] the path relative to the endpoint
|
75
|
+
# @param options [Hash] the parameters to supply
|
76
|
+
# @return [Response] the response
|
77
|
+
def delete(path, options = {})
|
78
|
+
request(:delete, path, form: options)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# @return [HTTP::Connection] the base connection object
|
84
|
+
def connection
|
85
|
+
# TODO: Make timeouts configurable
|
86
|
+
@connection ||= HTTP.persistent(@endpoint)
|
87
|
+
.headers('User-Agent' => @user_agent)
|
88
|
+
.timeout(write: 5, connect: 5, read: 5)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/redd/errors.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Redd
|
4
|
+
# Namespace for Redd errors.
|
5
|
+
module Errors
|
6
|
+
# An error raised by {Redd::Middleware} when there was an error returned by reddit.
|
7
|
+
class TokenRetrievalError < StandardError; end
|
8
|
+
|
9
|
+
# An error with the API.
|
10
|
+
class APIError < StandardError
|
11
|
+
attr_reader :response, :name
|
12
|
+
|
13
|
+
def initialize(response)
|
14
|
+
@response = response
|
15
|
+
super(response.body[:json][:errors][0].join(', '))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Indicates that you were rate limited. This should be taken care of automatically.
|
20
|
+
class RateLimitError < APIError
|
21
|
+
attr_reader :duration
|
22
|
+
|
23
|
+
def initialize(response)
|
24
|
+
super(response)
|
25
|
+
@duration = response.body[:json][:ratelimit]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Represents an error from reddit returned in a response.
|
30
|
+
class ResponseError < StandardError
|
31
|
+
attr_accessor :response
|
32
|
+
|
33
|
+
def initialize(response)
|
34
|
+
super(response.raw_body.length <= 80 ? response.raw_body : "#{response.raw_body[0..80]}...")
|
35
|
+
@response = response
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# An error returned by AuthStrategy.
|
40
|
+
# @note A common cause of this error is not having a bot account registered as a developer on
|
41
|
+
# the app.
|
42
|
+
class AuthenticationError < ResponseError; end
|
43
|
+
|
44
|
+
# An error with Redd, probably (let me know!)
|
45
|
+
class BadRequest < ResponseError; end
|
46
|
+
|
47
|
+
# Whatever it is, you're not allowed to do it.
|
48
|
+
class Forbidden < ResponseError; end
|
49
|
+
|
50
|
+
# You don't have the correct scope to do this.
|
51
|
+
class InsufficientScope < ResponseError; end
|
52
|
+
|
53
|
+
# The access object supplied was invalid.
|
54
|
+
class InvalidAccess < ResponseError; end
|
55
|
+
|
56
|
+
# Returned when reddit raises a 404 error.
|
57
|
+
class NotFound < ResponseError; end
|
58
|
+
|
59
|
+
# Too many requests and not enough rate limiting.
|
60
|
+
class TooManyRequests < ResponseError; end
|
61
|
+
|
62
|
+
# An unknown error on reddit's end. Usually fixed with a retry.
|
63
|
+
class ServerError < ResponseError; end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
require_relative '../redd'
|
7
|
+
|
8
|
+
module Redd
|
9
|
+
# Rack middleware.
|
10
|
+
class Middleware
|
11
|
+
# @param opts [Hash] the options to create the object with
|
12
|
+
# @option opts [String] :user_agent your app's *unique* and *descriptive* user agent
|
13
|
+
# @option opts [String] :client_id the client id of your app
|
14
|
+
# @option opts [String] :redirect_uri the provided redirect URI
|
15
|
+
# @option opts [String] :secret ('') the app secret (for the web type)
|
16
|
+
# @option opts [Array<String>] :scope (['identity']) a list of scopes to request
|
17
|
+
# @option opts ['temporary', 'permanent'] :duration ('permanent') the duration to request the
|
18
|
+
# code for.
|
19
|
+
# @option opts [Boolean] :auto_refresh (true) allow refreshing a permanent access automatically
|
20
|
+
# (only if duration is 'permanent')
|
21
|
+
# @option opts [String] :via ('/auth/reddit') the relative path in the application that
|
22
|
+
# redirects a user to reddit
|
23
|
+
def initialize(app, opts = {})
|
24
|
+
@app = app
|
25
|
+
strategy_opts = opts.select { |k| %i[user_agent client_id secret redirect_uri].include?(k) }
|
26
|
+
@strategy = Redd::AuthStrategies::Web.new(strategy_opts)
|
27
|
+
|
28
|
+
@user_agent = opts.fetch(:user_agent, "Redd:Web Application:v#{Redd::VERSION} (by unknown)")
|
29
|
+
@client_id = opts.fetch(:client_id)
|
30
|
+
@redirect_uri = opts.fetch(:redirect_uri)
|
31
|
+
@scope = opts.fetch(:scope, ['identity'])
|
32
|
+
@duration = opts.fetch(:duration, 'permanent')
|
33
|
+
@auto_refresh = opts.fetch(:auto_refresh, true) && @duration == 'permanent'
|
34
|
+
@via = opts.fetch(:via, '/auth/reddit')
|
35
|
+
end
|
36
|
+
|
37
|
+
def call(env)
|
38
|
+
# This is done for thread safety so that each thread has its own copy
|
39
|
+
# of the middleware logic.
|
40
|
+
dup._call(env)
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
def _call(env)
|
46
|
+
@request = Rack::Request.new(env)
|
47
|
+
return redirect_to_reddit! if @request.path == @via
|
48
|
+
|
49
|
+
before_call
|
50
|
+
response = @app.call(env)
|
51
|
+
after_call
|
52
|
+
response
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# Creates a unique state and redirects the user to reddit for authentication.
|
58
|
+
def redirect_to_reddit!
|
59
|
+
state = SecureRandom.urlsafe_base64
|
60
|
+
url = Redd.url(
|
61
|
+
client_id: @client_id,
|
62
|
+
redirect_uri: @redirect_uri,
|
63
|
+
scope: @scope,
|
64
|
+
duration: @duration,
|
65
|
+
state: state
|
66
|
+
)
|
67
|
+
@request.session[:redd_state] = state
|
68
|
+
[302, { 'Location' => url }, []]
|
69
|
+
end
|
70
|
+
|
71
|
+
# Do any setup before calling the rest of the application.
|
72
|
+
def before_call
|
73
|
+
# Convert the code to an access token if returning from authentication.
|
74
|
+
create_session! if @request.base_url + @request.path == @redirect_uri
|
75
|
+
# Clear the state for any other request.
|
76
|
+
@request.session.delete(:redd_state)
|
77
|
+
# Load a Session model from the access token in the user's cookies.
|
78
|
+
@request.env['redd.session'] = (@request.session[:redd_session] ? parse_session : nil)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Do any cleanup or changes after calling the application.
|
82
|
+
def after_call
|
83
|
+
env_session = @request.env['redd.session']
|
84
|
+
if env_session&.client&.access
|
85
|
+
# Make sure to flush any changes made to the Session client to the browser.
|
86
|
+
@request.session[:redd_session] = env_session.client.access.to_h
|
87
|
+
else
|
88
|
+
# Clear the session if the app explicitly set 'redd.session' to nil.
|
89
|
+
@request.session.delete(:redd_session)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Assigns a single string representing a reddit authentication errors.
|
94
|
+
def handle_token_error
|
95
|
+
message = nil
|
96
|
+
message = 'invalid_state' if @request.GET['state'] != @request.session[:redd_state]
|
97
|
+
message = @request.GET['error'] if @request.GET['error']
|
98
|
+
raise Errors::TokenRetrievalError, message if message
|
99
|
+
end
|
100
|
+
|
101
|
+
# Store the access token and other details in the user's browser, assigning any errors to
|
102
|
+
# the 'redd.error' env variable.
|
103
|
+
def create_session!
|
104
|
+
# Skip authorizing if there was an error from the authorization.
|
105
|
+
handle_token_error
|
106
|
+
# Try to get a code (the rescue block will also prevent crazy crashes)
|
107
|
+
access = @strategy.authenticate(@request.GET['code'])
|
108
|
+
@request.session[:redd_session] = access.to_h
|
109
|
+
rescue Errors::TokenRetrievalError, Errors::ResponseError => e
|
110
|
+
@request.env['redd.error'] = e
|
111
|
+
end
|
112
|
+
|
113
|
+
# Return a {Redd::Models::Session} based on the hash saved into the browser's session.
|
114
|
+
def parse_session
|
115
|
+
parsed_session = @request.session[:redd_session].transform_keys(&to_sym)
|
116
|
+
|
117
|
+
client = Redd::APIClient.new(@strategy,
|
118
|
+
user_agent: @user_agent,
|
119
|
+
limit_time: 0,
|
120
|
+
auto_refresh: @auto_refresh)
|
121
|
+
client.access = Redd::Models::Access.new(@strategy, parsed_session)
|
122
|
+
Redd::Models::Session.new(client)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'model'
|
4
|
+
|
5
|
+
module Redd
|
6
|
+
module Models
|
7
|
+
# Models access_token and related keys.
|
8
|
+
# @note This model also supports an additional key, called `:created_at` which is a UNIX time
|
9
|
+
# representing the time the access was created. The default value is the time the object was
|
10
|
+
# initialized.
|
11
|
+
class Access < Model
|
12
|
+
# Create a non-lazily initialized Access.
|
13
|
+
# @param attributes [Hash] the access's attributes
|
14
|
+
# @example
|
15
|
+
# access = Redd::Models::Access.new(access_token: ...)
|
16
|
+
def initialize(attributes = {})
|
17
|
+
super(nil, attributes)
|
18
|
+
@creation_time = Time.now
|
19
|
+
end
|
20
|
+
|
21
|
+
# Whether the access has expired.
|
22
|
+
# @param grace_period [Integer] the grace period where the model expires early
|
23
|
+
# @return [Boolean] whether the access has expired
|
24
|
+
def expired?(grace_period = 60)
|
25
|
+
Time.now > read_attribute(:created_at) + read_attribute(:expires_in) - grace_period
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [Boolean] whether the access can be refreshed
|
29
|
+
def permanent?
|
30
|
+
read_attribute(:refresh_token).nil?
|
31
|
+
end
|
32
|
+
|
33
|
+
# @!attribute [r] access_token
|
34
|
+
# @return [String] the access token
|
35
|
+
property :access_token
|
36
|
+
|
37
|
+
# @!attribute [r] refresh_token
|
38
|
+
# @return [String] the (optional) refresh token
|
39
|
+
property :refresh_token, :nil
|
40
|
+
|
41
|
+
# @!attribute [r] expires_in
|
42
|
+
# @return [Integer] the number of seconds before the access expires
|
43
|
+
property :expires_in
|
44
|
+
|
45
|
+
# @!attribute [r] created_at
|
46
|
+
# @return [Time] the time the access was created
|
47
|
+
property :created_at, default: -> { @creation_time }
|
48
|
+
|
49
|
+
# @!attribute [r] scope
|
50
|
+
# @return [Array<String>] the scopes that the user is allowed to access
|
51
|
+
property :scope, with: ->(scope) { scope.split(' ') }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,229 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'model'
|
4
|
+
require_relative 'gildable'
|
5
|
+
require_relative 'inboxable'
|
6
|
+
require_relative 'moderatable'
|
7
|
+
require_relative 'postable'
|
8
|
+
require_relative 'replyable'
|
9
|
+
require_relative 'reportable'
|
10
|
+
|
11
|
+
require_relative 'listing'
|
12
|
+
require_relative 'subreddit'
|
13
|
+
require_relative 'user'
|
14
|
+
|
15
|
+
module Redd
|
16
|
+
module Models
|
17
|
+
# A comment.
|
18
|
+
class Comment < Model
|
19
|
+
include Gildable
|
20
|
+
include Inboxable
|
21
|
+
include Moderatable
|
22
|
+
include Postable
|
23
|
+
include Replyable
|
24
|
+
include Reportable
|
25
|
+
|
26
|
+
# @!attribute [r] subreddit_id
|
27
|
+
# @return [String] the subreddit fullname
|
28
|
+
property :subreddit_id
|
29
|
+
|
30
|
+
# @!attribute [r] approved_at
|
31
|
+
# @return [Time, nil] the time when the comment was approved
|
32
|
+
property :approved_at, from: :approved_at_utc, with: ->(t) { Time.at(t) if t }
|
33
|
+
|
34
|
+
# @!attribute [r] banned_by
|
35
|
+
# @return [String] the user (?) that banned this comment
|
36
|
+
property :banned_by
|
37
|
+
|
38
|
+
# @!attribute [r] removal_reason
|
39
|
+
# @return [String, nil] the reason for comment removal
|
40
|
+
property :removal_reason
|
41
|
+
|
42
|
+
# @!attribute [r] link
|
43
|
+
# @return [Submission] the link that the comment was posted to
|
44
|
+
property :link, from: :link_id, with: ->(id) { Submission.new(client, name: id) }
|
45
|
+
|
46
|
+
# @!attribute [r] upvoted?
|
47
|
+
# @return [Boolean, nil] whether the user liked/disliked this comment
|
48
|
+
property :upvoted?, from: :likes
|
49
|
+
|
50
|
+
# @!attribute [r] replies
|
51
|
+
# @return [Listing<Comment>] the comment replies
|
52
|
+
property :replies,
|
53
|
+
with: ->(r) { r.is_a?(Hash) ? Listing.new(client, r[:data]) : Listing.empty(client) }
|
54
|
+
|
55
|
+
# @!attribute [r] user_reports
|
56
|
+
# @return [Array<String>] user reports
|
57
|
+
property :user_reports
|
58
|
+
|
59
|
+
# @!attribute [r] saved?
|
60
|
+
# @return [Boolean] whether the submission was saved by the logged-in user
|
61
|
+
property :saved?, from: :saved
|
62
|
+
|
63
|
+
# @!attribute [r] id
|
64
|
+
# @return [String] the comment id
|
65
|
+
property :id
|
66
|
+
|
67
|
+
# @!attribute [r] title
|
68
|
+
# @return ["comment reply", "post reply", "username mention"] the comment "title" (only
|
69
|
+
# visible in messages)
|
70
|
+
property :title, :nil
|
71
|
+
|
72
|
+
# @!attribute [r] banned_at
|
73
|
+
# @return [Time, nil] the time when the comment was banned
|
74
|
+
property :banned_at, from: :banned_at_utc, with: ->(t) { Time.at(t) if t }
|
75
|
+
|
76
|
+
# @!attribute [r] gilded
|
77
|
+
# @return [Integer] the number of times the comment was gilded
|
78
|
+
property :gilded
|
79
|
+
|
80
|
+
# @!attribute [r] archived?
|
81
|
+
# @return [Boolean] whether this comment was archived
|
82
|
+
property :archived?, from: :archived
|
83
|
+
|
84
|
+
# @!attribute [r] report_reasons
|
85
|
+
# @return [Array<String>] report reasons
|
86
|
+
property :report_reasons
|
87
|
+
|
88
|
+
# @!attribute [r] author
|
89
|
+
# @return [User] the comment author
|
90
|
+
property :author, with: ->(name) { User.new(client, name: name) }
|
91
|
+
|
92
|
+
# @!attribute [r] can_mod_post?
|
93
|
+
# @return [Boolean] whether the logged-in user can mod post
|
94
|
+
property :can_mod_post?, from: :can_mod_post
|
95
|
+
|
96
|
+
# @!attribute [r] ups
|
97
|
+
# @return [Integer] the comment upvotes
|
98
|
+
# @deprecated use {#score} instead
|
99
|
+
property :ups
|
100
|
+
|
101
|
+
# @!attribute [r] downs
|
102
|
+
# @return [Integer] the comment downvotes
|
103
|
+
# @deprecated is always 0; use {#score} instead
|
104
|
+
property :downs
|
105
|
+
|
106
|
+
# @!attribute [r] parent_id
|
107
|
+
# @return [String] the parent fullname
|
108
|
+
property :parent_id
|
109
|
+
|
110
|
+
# @!attribute [r] parent
|
111
|
+
# @return [Comment, Submission] the comment parent
|
112
|
+
property :parent,
|
113
|
+
from: :parent_id,
|
114
|
+
with: ->(id) { Session.new(client).from_fullnames(id).first }
|
115
|
+
|
116
|
+
# @!attribute [r] score
|
117
|
+
# @return [Integer] the comment score
|
118
|
+
property :score
|
119
|
+
|
120
|
+
# @!attribute [r] approved_by
|
121
|
+
# @return [String] the user that approved the comment
|
122
|
+
property :approved_by
|
123
|
+
|
124
|
+
# @!attribute [r] body
|
125
|
+
# @return [String] the markdown comment body
|
126
|
+
property :body
|
127
|
+
|
128
|
+
# @!attribute [r] body_html
|
129
|
+
# @return [String] the html-rendered version of the body
|
130
|
+
property :body_html
|
131
|
+
|
132
|
+
# @!attribute [r] edited_at
|
133
|
+
# @return [Time, nil] the time when the comment was edited
|
134
|
+
property :edited_at, from: :edited, with: ->(t) { Time.at(t) if t }
|
135
|
+
|
136
|
+
# @!attribute [r] author_flair_css_class
|
137
|
+
# @return [String] the author flair css class
|
138
|
+
property :author_flair_css_class
|
139
|
+
|
140
|
+
# @!attribute [r] collapsed?
|
141
|
+
# @return [Boolean] whether the comment was collapsed
|
142
|
+
property :collapsed?, from: :collapsed
|
143
|
+
|
144
|
+
# @!attribute [r] submitter?
|
145
|
+
# @return [Boolean] whether the comment author is the link submitter
|
146
|
+
property :submitter?, from: :is_submitter
|
147
|
+
|
148
|
+
# @!attribute [r] collapsed_reason
|
149
|
+
# @return [String] the reason for collapse (?)
|
150
|
+
property :collapsed_reason
|
151
|
+
|
152
|
+
# @!attribute [r] stickied?
|
153
|
+
# @return [Boolean] whether the comment was stickied
|
154
|
+
property :stickied?, from: :stickied
|
155
|
+
|
156
|
+
# @!attribute [r] can_gild?
|
157
|
+
# @return [Boolean] whether the comment is gildable
|
158
|
+
property :can_gild?, from: :can_gild
|
159
|
+
|
160
|
+
# @!attribute [r] subreddit
|
161
|
+
# @return [Subreddit] the comment's subreddit
|
162
|
+
property :subreddit, with: ->(n) { Subreddit.new(client, display_name: n) }
|
163
|
+
|
164
|
+
# @!attribute [r] score_hidden
|
165
|
+
# @return [Boolean] whether the comment score is hidden
|
166
|
+
property :score_hidden?, from: :score_hidden
|
167
|
+
|
168
|
+
# @!attribute [r] subreddit_type
|
169
|
+
# @return [String] subreddit type
|
170
|
+
property :subreddit_type
|
171
|
+
|
172
|
+
# @!attribute [r] name
|
173
|
+
# @return [String] the comment fullname
|
174
|
+
property :name
|
175
|
+
|
176
|
+
# @!attribute [r] author_flair_text
|
177
|
+
# @return [String] the author flair text
|
178
|
+
property :author_flair_text
|
179
|
+
|
180
|
+
# @!attribute [r] created_at
|
181
|
+
# @return [String] the time when the model was created
|
182
|
+
property :created_at, from: :created_utc, with: ->(t) { Time.at(t) }
|
183
|
+
|
184
|
+
# @!attribute [r] subreddit_name_prefixed
|
185
|
+
# @return [String] the subreddit name, prefixed with "r/"
|
186
|
+
property :subreddit_name_prefixed
|
187
|
+
|
188
|
+
# @!attribute [r] controversiality
|
189
|
+
# @return [Integer] the comment controversiality
|
190
|
+
property :controversiality
|
191
|
+
|
192
|
+
# @!attribute [r] depth
|
193
|
+
# @return [Integer] the comment depth
|
194
|
+
property :depth
|
195
|
+
|
196
|
+
# @!attribute [r] mod_reports
|
197
|
+
# @return [Array<String>] the moderator reports
|
198
|
+
property :mod_reports
|
199
|
+
|
200
|
+
# @!attribute [r] report_count
|
201
|
+
# @return [Integer] the report count
|
202
|
+
property :report_count, from: :num_reports
|
203
|
+
|
204
|
+
# @!attribute [r] distinguished?
|
205
|
+
# @return [Boolean] whether the comment is distinguished
|
206
|
+
property :distinguished?, from: :distinguished
|
207
|
+
|
208
|
+
private
|
209
|
+
|
210
|
+
def lazer_reload
|
211
|
+
exists_locally?(:link) ? load_with_comments : load_without_comments
|
212
|
+
end
|
213
|
+
|
214
|
+
def load_with_comments
|
215
|
+
fully_loaded!
|
216
|
+
id = exists_locally?(:id) ? read_attribute(:id) : read_attribute(:name).sub('t1_', '')
|
217
|
+
link_id = read_attribute(:link).name.sub('t3_', '')
|
218
|
+
client.get("/comments/#{link_id}/_/#{id}").body[1][:data][:children][0][:data]
|
219
|
+
end
|
220
|
+
|
221
|
+
def load_without_comments
|
222
|
+
id = exists_locally?(:id) ? read_attribute(:id) : read_attribute(:name).sub('t1_', '')
|
223
|
+
response = client.get('/api/info', id: "t1_#{id}").body[:data][:children][0][:data]
|
224
|
+
response.delete(:replies) # Make sure replies are lazy-loaded later.
|
225
|
+
response
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'model'
|
4
|
+
|
5
|
+
module Redd
|
6
|
+
module Models
|
7
|
+
# The front page.
|
8
|
+
# FIXME: deal with serious code duplication from Subreddit
|
9
|
+
class FrontPage < Model
|
10
|
+
# @return [Array<String>] reddit's base wiki pages
|
11
|
+
def wiki_pages
|
12
|
+
client.get('/wiki/pages').body[:data]
|
13
|
+
end
|
14
|
+
|
15
|
+
# Get a wiki page by its title.
|
16
|
+
# @param title [String] the page's title
|
17
|
+
# @return [WikiPage]
|
18
|
+
def wiki_page(title)
|
19
|
+
WikiPage.new(client, title: title)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Get the appropriate listing.
|
23
|
+
# @param sort [:hot, :new, :top, :controversial, :comments, :rising, :gilded] the type of
|
24
|
+
# listing
|
25
|
+
# @param options [Hash] a list of options to send with the request
|
26
|
+
# @option options [String] :after return results after the given fullname
|
27
|
+
# @option options [String] :before return results before the given fullname
|
28
|
+
# @option options [Integer, nil] :limit maximum number of items to return (nil for no limit)
|
29
|
+
# @option options [:hour, :day, :week, :month, :year, :all] :time the time period to consider
|
30
|
+
# when sorting.
|
31
|
+
#
|
32
|
+
# @note The option :time only applies to the top and controversial sorts.
|
33
|
+
# @return [PaginatedListing<Submission>]
|
34
|
+
def listing(sort, **options)
|
35
|
+
options[:t] = options.delete(:time) if options.key?(:time)
|
36
|
+
PaginatedListing.new(client, **options) do |**req_options|
|
37
|
+
client.model(:get, "/#{sort}", options.merge(req_options))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# @!method hot(**params)
|
42
|
+
# @!method new(**params)
|
43
|
+
# @!method top(**params)
|
44
|
+
# @!method controversial(**params)
|
45
|
+
# @!method comments(**params)
|
46
|
+
# @!method rising(**params)
|
47
|
+
# @!method gilded(**params)
|
48
|
+
#
|
49
|
+
# @see #listing
|
50
|
+
%i[hot new top controversial comments rising gilded].each do |sort|
|
51
|
+
define_method(sort) { |**params| listing(sort, **params) }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Redd
|
4
|
+
module Models
|
5
|
+
# A model that can be gilded.
|
6
|
+
module Gildable
|
7
|
+
# Gift a user one month of reddit gold for their link or comment.
|
8
|
+
def gild
|
9
|
+
client.post("/api/v1/gold/gild/#{read_attribute(:name)}")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Redd
|
4
|
+
module Models
|
5
|
+
# Things that can be sent to a user's inbox.
|
6
|
+
module Inboxable
|
7
|
+
# Block the user that sent this item.
|
8
|
+
def block
|
9
|
+
client.post('/api/block', id: read_attribute(:name))
|
10
|
+
end
|
11
|
+
|
12
|
+
# Collapse the item.
|
13
|
+
def collapse
|
14
|
+
client.post('/api/collapse_message', id: read_attribute(:name))
|
15
|
+
end
|
16
|
+
|
17
|
+
# Uncollapse the item.
|
18
|
+
def uncollapse
|
19
|
+
client.post('/api/uncollapse_message', id: read_attribute(:name))
|
20
|
+
end
|
21
|
+
|
22
|
+
# Mark this thing as read.
|
23
|
+
def mark_as_read
|
24
|
+
client.post('/api/read_message', id: read_attribute(:name))
|
25
|
+
end
|
26
|
+
|
27
|
+
# Mark one or more messages as unread.
|
28
|
+
def mark_as_unread
|
29
|
+
client.post('/api/unread_message', id: read_attribute(:name))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|