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