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,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model'
4
+ require_relative 'searchable'
5
+
6
+ module Redd
7
+ module Models
8
+ # The starter class.
9
+ class Session < Model
10
+ include Searchable
11
+
12
+ # @return [User] the logged-in user
13
+ def me
14
+ @me ||= Self.new(client)
15
+ end
16
+
17
+ # @return [FrontPage] the user's front page
18
+ def front_page
19
+ @front_page ||= FrontPage.new(client)
20
+ end
21
+
22
+ # @return [Modmail] the new modmail
23
+ def modmail
24
+ @modmail ||= Modmail.new(client)
25
+ end
26
+
27
+ # @return [LiveThread] the live thread
28
+ def live_thread(id)
29
+ LiveThread.new(client, id: id)
30
+ end
31
+
32
+ # @return [Hash] a breakdown of karma over subreddits
33
+ def karma_breakdown
34
+ client.get('/api/v1/me/karma').body[:data]
35
+ end
36
+
37
+ # Get a (lazily loaded) reddit user by their name.
38
+ # @param name [String] the username
39
+ # @return [User]
40
+ def user(name)
41
+ User.new(client, name: name)
42
+ end
43
+
44
+ # Returns whether a username is available.
45
+ # @param username [String] the username to check
46
+ # @return [Boolean] whether the username is available
47
+ def username_available?(username)
48
+ client.get('/api/username_available', user: username).body
49
+ end
50
+
51
+ # Get a (lazily loaded) subreddit by its name.
52
+ # @param display_name [String] the subreddit's display name
53
+ # @return [Subreddit]
54
+ def subreddit(display_name)
55
+ Subreddit.new(client, display_name: display_name)
56
+ end
57
+
58
+ # @return [Array<Multireddit>] array of multireddits belonging to the user
59
+ def my_multis
60
+ client.get('/api/multi/mine').body.map { |m| client.unmarshal(m) }
61
+ end
62
+
63
+ # Get a (lazily loaded) multi by its path.
64
+ # @param path [String] the multi's path, surrounded by a leading and trailing /
65
+ # @return [Multireddit]
66
+ def multi(path)
67
+ Multireddit.new(client, path: path)
68
+ end
69
+
70
+ # Get submissions or comments by their fullnames.
71
+ # @param fullnames [String, Array<String>] one or an array of fullnames (e.g. t3_abc1234)
72
+ # @return [Listing<Submission, Comment>]
73
+ # @deprecated Try the lazier {#from_fullnames} instead.
74
+ def from_ids(*fullnames)
75
+ client.model(:get, '/api/info', id: fullnames.join(','))
76
+ end
77
+
78
+ # Create lazily-loaded objects from their fullnames (e.g. t1_abc123).
79
+ # @param fullnames [String] fullname for a submission, comment, or subreddit.
80
+ # @return [Array<Submission, Comment, User, Subreddit>]
81
+ def from_fullnames(*fullnames) # rubocop:disable Metrics/MethodLength
82
+ fullnames.map do |name|
83
+ if name.start_with?('t1_')
84
+ Comment.new(client, name: name)
85
+ elsif name.start_with?('t3_')
86
+ Submission.new(client, name: name)
87
+ elsif name.start_with?('t5_')
88
+ Subreddit.new(client, name: name)
89
+ else
90
+ raise "unknown fullname #{name}"
91
+ end
92
+ end
93
+ end
94
+
95
+ # Get submissions or comments by their fullnames.
96
+ # @param url [String] the object's url
97
+ # @return [Submission, Comment, nil] the object, or nil if not found
98
+ def from_url(url)
99
+ client.model(:get, '/api/info', url: url).first
100
+ end
101
+
102
+ # Return a listing of the user's inbox (including comment replies and private messages).
103
+ #
104
+ # @param category ['inbox', 'unread', 'sent', 'moderator', 'messages', 'comments',
105
+ # 'selfreply', 'mentions'] the category of messages to view
106
+ # @param mark [Boolean] whether to remove the orangered from the user's inbox
107
+ # @param params [Hash] a list of optional params to send with the request
108
+ # @option params [String] :after return results after the given fullname
109
+ # @option params [String] :before return results before the given fullname
110
+ # @option params [Integer] :count (0) the number of items already seen in the listing
111
+ # @option params [1..100] :limit (25) the maximum number of things to return
112
+ # @return [Listing<Comment, PrivateMessage>]
113
+ def my_messages(category: 'inbox', mark: false, **params)
114
+ client.model(:get, "/message/#{category}.json", params.merge(mark: mark))
115
+ end
116
+
117
+ # Mark all messages as read.
118
+ def read_all_messages
119
+ client.post('/api/read_all_messages')
120
+ end
121
+
122
+ # @return [Hash] the user's preferences
123
+ def my_preferences
124
+ client.get('/api/v1/me/prefs').body
125
+ end
126
+
127
+ # Edit the user's preferences.
128
+ # @param new_prefs [Hash] the changed preferences
129
+ # @return [Hash] the new preferences
130
+ # @see #my_preferences
131
+ def edit_preferences(new_prefs = {})
132
+ client.request(
133
+ :patch, '/api/v1/me/prefs',
134
+ headers: { 'Content-Type' => 'application/json' },
135
+ body: JSON.generate(new_prefs)
136
+ ).body
137
+ end
138
+
139
+ # @return [Array<User>] the logged-in user's friends
140
+ def friends
141
+ client.get('/api/v1/me/friends').body[:data][:children].map do |h|
142
+ User.new(client, name: h[:name], id: h[:id].sub('t2_', ''), since: h[:date])
143
+ end
144
+ end
145
+
146
+ # @return [Array<User>] users blocked by the logged-in user
147
+ def blocked
148
+ client.get('/prefs/blocked').body[:data][:children].map do |h|
149
+ User.new(client, name: h[:name], id: h[:id].sub('t2_', ''), since: h[:date])
150
+ end
151
+ end
152
+
153
+ # @return [Array<User>] users trusted by the logged-in user
154
+ def trusted
155
+ client.get('/prefs/trusted').body[:data][:children].map do |h|
156
+ User.new(client, name: h[:name], id: h[:id].sub('t2_', ''), since: h[:date])
157
+ end
158
+ end
159
+
160
+ # @return [Array<String>] a list of categories the user's items are saved in
161
+ def saved_categories
162
+ client.get('/api/saved_categories').body
163
+ end
164
+
165
+ # Return a listing of the user's subreddits.
166
+ #
167
+ # @param type ['subscriber', 'contributor', 'moderator'] the type of subreddits
168
+ # @param params [Hash] a list of optional params to send with the request
169
+ # @option params [String] :after return results after the given fullname
170
+ # @option params [String] :before return results before the given fullname
171
+ # @option params [Integer] :count (0) the number of items already seen in the listing
172
+ # @option params [1..100] :limit (25) the maximum number of things to return
173
+ # @return [Listing<Subreddit>]
174
+ def my_subreddits(type, **params)
175
+ client.model(:get, "/subreddits/mine/#{type}", params)
176
+ end
177
+
178
+ # Return trending subreddits.
179
+ # @return [Hash]
180
+ # @example
181
+ # session.trending_subreddits
182
+ # => {
183
+ # "subreddit_names": [
184
+ # "AskLibertarians",
185
+ # "OpTicGaming",
186
+ # "Cuphead",
187
+ # "AlmostParkour",
188
+ # "TheGoodPlace"
189
+ # ],
190
+ # "comment_count": 176,
191
+ # "comment_url": "/r/trendingsubreddits/comments/73dkin/trending_subreddits_for_..."
192
+ # }
193
+ def trending_subreddits
194
+ client.get('/api/trending_subreddits').body
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,405 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model'
4
+ require_relative 'gildable'
5
+ require_relative 'moderatable'
6
+ require_relative 'postable'
7
+ require_relative 'replyable'
8
+ require_relative 'reportable'
9
+
10
+ require_relative 'user'
11
+ require_relative 'subreddit'
12
+
13
+ module Redd
14
+ module Models
15
+ # A text or link post.
16
+ class Submission < Model # rubocop:disable Metrics/ClassLength
17
+ include Gildable
18
+ include Moderatable
19
+ include Postable
20
+ include Replyable
21
+ include Reportable
22
+
23
+ # @return [String] the sort order
24
+ def sort_order
25
+ exists_locally?(:sort_order) ? read_attribute(:sort_order) : nil
26
+ end
27
+
28
+ # Set the sort order of the comments and reload the comments.
29
+ # @param new_order [:confidence, :top, :controversial, :old, :qa] the sort order
30
+ def update_sort_order(new_order)
31
+ return self if new_order == read_attribute(:sort_order)
32
+
33
+ write_attribute(:sort_order, new_order)
34
+ reload
35
+ end
36
+
37
+ # Get all submissions for the same url.
38
+ # @param params [Hash] A list of optional params to send with the request.
39
+ # @option params [String] :after return results after the given fullname
40
+ # @option params [String] :before return results before the given fullname
41
+ # @option params [Integer] :count (0) the number of items already seen in the listing
42
+ # @option params [1..100] :limit (25) the maximum number of things to return
43
+ # @return [Listing<Submission>]
44
+ def duplicates(**params)
45
+ client.unmarshal(client.get("/duplicates/#{read_attribute(:id)}", params).body[1])
46
+ end
47
+
48
+ # Mark the link as "Not Suitable For Work".
49
+ def mark_as_nsfw
50
+ client.get('/api/marknsfw', id: read_attribute(:name))
51
+ end
52
+
53
+ # No longer mark the link as "Not Suitable For Work".
54
+ def unmark_as_nsfw
55
+ client.get('/api/unmarknsfw', id: read_attribute(:name))
56
+ end
57
+
58
+ # Mark the link as a spoiler.
59
+ def mark_as_spoiler
60
+ client.get('/api/spoiler', id: read_attribute(:name))
61
+ end
62
+
63
+ # No longer mark the link as a spoiler.
64
+ def unmark_as_spoiler
65
+ client.get('/api/unspoiler', id: read_attribute(:name))
66
+ end
67
+
68
+ # Set the submission to "contest mode" (comments are randomly sorted)
69
+ def enable_contest_mode
70
+ client.post('/api/set_contest_mode', id: read_attribute(:name), state: true)
71
+ end
72
+
73
+ # Disable the "contest mode".
74
+ def disable_contest_mode
75
+ client.post('/api/set_contest_mode', id: read_attribute(:name), state: false)
76
+ end
77
+
78
+ # Set the submission as the sticky post of the subreddit.
79
+ # @param slot [1, 2] which "slot" to place the sticky on
80
+ def make_sticky(slot: nil)
81
+ client.post('/api/set_subreddit_sticky', id: read_attribute(:name), num: slot, state: true)
82
+ end
83
+
84
+ # Unsticky the post from the subreddit.
85
+ def remove_sticky
86
+ client.post('/api/set_subreddit_sticky', id: read_attribute(:name), state: false)
87
+ end
88
+
89
+ # Prevent users from commenting on the link (and hide it as well).
90
+ def lock
91
+ client.post('/api/lock', id: read_attribute(:name))
92
+ end
93
+
94
+ # Allow users to comment on the link again.
95
+ def unlock
96
+ client.post('/api/unlock', id: read_attribute(:name))
97
+ end
98
+
99
+ # Set the suggested sort order for comments for all users.
100
+ # @param suggested ['blank', 'confidence', 'top', 'new', 'controversial', 'old', 'random',
101
+ # 'qa', 'live'] the sort type
102
+ def set_suggested_sort(suggested) # rubocop:disable Naming/AccessorMethodName
103
+ client.post('/api/set_suggested_sort', id: read_attribute(:name), sort: suggested)
104
+ end
105
+
106
+ # @!attribute [r] sort_order
107
+ # @return [Symbol] the comment sort order
108
+ property :sort_order, :nil
109
+
110
+ # @!attribute [r] comments
111
+ # @return [Array<Comment>] the comment tree
112
+ property :comments, :nil, with: ->(l) { Listing.new(client, l) if l }
113
+
114
+ # @!attribute [r] domain
115
+ # @return [String] the domain name of the link (or self.subreddit_name)
116
+ property :domain
117
+
118
+ # @!attribute [r] approved_at
119
+ # @return [Time, nil] when the submission was last approved
120
+ property :approved_at, from: :approved_at_utc, with: ->(t) { Time.at(t) if t }
121
+
122
+ # @!attribute [r] banned_by
123
+ # @return [String] not sure what this does
124
+ property :banned_by
125
+
126
+ # @!attribute [r] media_embed
127
+ # @return [Hash] media embed properties
128
+ property :media_embed
129
+
130
+ # @!attribute [r] subreddit
131
+ # @return [Subreddit] the subreddit the post belongs to.
132
+ property :subreddit, with: ->(n) { Subreddit.new(client, display_name: n) }
133
+
134
+ # @!attribute [r] selftext_html
135
+ # @return [String, nil] the html-rendered self text, can be nil if link
136
+ property :selftext_html
137
+
138
+ # @!attribute [r] selftext
139
+ # @return [String] the self text contents
140
+ property :selftext
141
+
142
+ # @!attribute [r] upvoted?
143
+ # @return [Boolean, nil] whether the user upvoted/downvoted this post
144
+ property :upvoted?, from: :likes
145
+
146
+ # @!attribute [r] suggested_sort
147
+ # @return [String, nil] the suggested sort
148
+ property :suggested_sort
149
+
150
+ # @!attribute [r] user_reports
151
+ # @return [Array<String>] user reports
152
+ property :user_reports
153
+
154
+ # @!attribute [r] secure_media
155
+ # @return [Hash, nil] secure media details
156
+ property :secure_media
157
+
158
+ # @!attribute [r] link_flair_text
159
+ # @return [String] the link flair text
160
+ property :link_flair_text
161
+
162
+ # @!attribute [r] link_flair_css_class
163
+ # @return [String] the link flair css class
164
+ property :link_flair_css_class
165
+
166
+ # @!attribute [r] id
167
+ # @return [String] the submission id
168
+ property :id
169
+
170
+ # @!attribute [r] banned_at
171
+ # @return [Time, nil] the time the post was banned
172
+ property :banned_at, from: :banned_at_utc, with: ->(t) { Time.at(t) if t }
173
+
174
+ # @!attribute [r] view_count
175
+ # @return [Integer, nil] the view count
176
+ property :view_count
177
+
178
+ # @!attribute [r] archived?
179
+ # @return [Boolean] whether the post is archived
180
+ property :archived?, from: :archived
181
+
182
+ # @!attribute [r] clicked?
183
+ # @return [Boolean] whether the post was clicked
184
+ property :clicked?, from: :clicked
185
+
186
+ # @!attribute [r] report_reasons
187
+ # @return [Object] i think it's an array of strings?
188
+ property :report_reasons
189
+
190
+ # @!attribute [r] title
191
+ # @return [String] the post title
192
+ property :title
193
+
194
+ # @!attribute [r] num_crossposts
195
+ # @return [Integer] the number of crossposts made to other subreddits
196
+ property :num_crossposts
197
+
198
+ # @!attribute [r] saved?
199
+ # @return [String] whether the post was saved by the logged-in user
200
+ property :saved?, from: :saved
201
+
202
+ # @!attribute [r] can_mod_post?
203
+ # @return [Boolean] whether you can post as a mod, i think
204
+ property :can_mod_post?, from: :can_mod_post
205
+
206
+ # @!attribute [r] crosspostable?
207
+ # @return [Boolean] whether the post can be crossposted
208
+ property :crosspostable?, from: :is_crosspostable
209
+
210
+ # @!attribute [r] pinned?
211
+ # @return [Boolean] whether the post is pinned
212
+ property :pinned?, from: :pinned
213
+
214
+ # @!attribute [r] score
215
+ # @return [Integer] the post's score
216
+ property :score
217
+
218
+ # @!attribute [r] approved_by
219
+ # @return [Object] the person that approved this post (not sure about the schema)
220
+ property :approved_by
221
+
222
+ # @!attribute [r] over_18?
223
+ # @return [Boolean] whether the post is marked as over 18
224
+ property :over_18?, from: :over_18
225
+
226
+ # @!attribute [r] hidden?
227
+ # @return [Boolean] whether the logged-in user hid the post
228
+ property :hidden?, from: :hidden
229
+
230
+ # @!attribute [r] preview
231
+ # @return [Hash] preview details
232
+ property :preview
233
+
234
+ # @!attribute [r] comment_count
235
+ # @return [Integer] the post's comment count
236
+ property :comment_count, from: :num_comments
237
+
238
+ # @!attribute [r] thumbnail
239
+ # @return [String] the thumbnail url
240
+ property :thumbnail
241
+
242
+ # @!attribute [r] thumbnail_height
243
+ # @return [Integer] thumbnail height
244
+ property :thumbnail_height
245
+
246
+ # @!attribute [r] score_hidden?
247
+ # @return [Boolean] whether the score is hidden
248
+ property :score_hidden?, from: :hide_score
249
+
250
+ # @!attribute [r] edited
251
+ # @return [Time, false] the time of the last edit
252
+ property :edited, with: ->(t) { Time.at(t) if t }
253
+
254
+ # @!attribute [r] author_flair_text
255
+ # @return [String] the author flair text
256
+ property :author_flair_text
257
+
258
+ # @!attribute [r] author_flair_css_class
259
+ # @return [String] the author flair css class
260
+ property :author_flair_css_class
261
+
262
+ # @!attribute [r] contest_mode_enabled?
263
+ # @return [Boolean] whether contest mode is turned on
264
+ property :contest_mode_enabled?, from: :contest_mode
265
+
266
+ # @!attribute [r] gilded
267
+ # @return [Integer] the number of times the post was gilded
268
+ property :gilded
269
+
270
+ # @!attribute [r] locked?
271
+ # @return [Boolean] whether the post is locked
272
+ property :locked?, from: :locked
273
+
274
+ # @!attribute [r] ups
275
+ # @return [Integer] upvote count
276
+ # @deprecated this doesn't return the raw upvote count - use {#score} instead
277
+ property :ups
278
+
279
+ # @!attribute [r] downs
280
+ # @return [Integer] downvote count
281
+ # @deprecated this always returns zero - use {#score} instead
282
+ property :downs
283
+
284
+ # @!attribute [r] brand_safe?
285
+ # @return [Boolean] whether the post is marked as brand safe
286
+ property :brand_safe?, from: :brand_safe
287
+
288
+ # @!attribute [r] secure_media_embed
289
+ # @return [Hash] secure media embed details
290
+ property :secure_media_embed
291
+
292
+ # @!attribute [r] removal_reason
293
+ # @return [String] the removal reason
294
+ property :removal_reason
295
+
296
+ # @!attribute [r] post_hint
297
+ # @return [String] a hint at what the link should contain
298
+ property :post_hint
299
+
300
+ # @!attribute [r] can_gild?
301
+ # @return [Boolean] whether the user can gild this post
302
+ property :can_gild?, from: :can_gild
303
+
304
+ # @!attribute [r] parent_whitelist_status
305
+ # @return [String] ad-related whitelist stuff
306
+ property :parent_whitelist_status
307
+
308
+ # @!attribute [r] name
309
+ # @return [String] the fullname (i.e. t3_xxxxxx)
310
+ property :name
311
+
312
+ # @!attribute [r] spoiler?
313
+ # @return [Boolean] whether the post was marked as a spoiler
314
+ property :spoiler?, from: :spoiler
315
+
316
+ # @!attribute [r] permalink
317
+ # @return [String] the **relative** url permalink
318
+ property :permalink
319
+
320
+ # @!attribute [r] report_count
321
+ # @return [Integer] the number of reports
322
+ property :report_count, from: :num_reports
323
+
324
+ # @!attribute [r] whitelist_status
325
+ # @return [String] ad-related whitelist stuff
326
+ property :whitelist_status
327
+
328
+ # @!attribute [r] stickied?
329
+ # @return [Boolean] whether the post was stickied
330
+ property :stickied?, from: :stickied
331
+
332
+ # @!attribute [r] url
333
+ # @return [String] the link url
334
+ property :url
335
+
336
+ # @!attribute [r] quarantined?
337
+ # @return [Boolean] whether the post has been quarantined
338
+ property :quarantined?, from: :quarantine
339
+
340
+ # @!attribute [r] author
341
+ # @return [User] the post author
342
+ property :author, with: ->(n) { User.new(client, name: n) }
343
+
344
+ # @!attribute [r] created_at
345
+ # @return [Time] creation time
346
+ property :created_at, from: :created_utc, with: ->(t) { Time.at(t) }
347
+
348
+ # @!attribute [r] subreddit_name_prefixed
349
+ # @return [String] r/[subreddit name]
350
+ property :subreddit_name_prefixed,
351
+ default: -> { "r/#{read_attribute(:subreddit).display_name}" }
352
+
353
+ # @!attribute [r] distinguished?
354
+ # @return [Boolean] whether the post is distinguished
355
+ property :distinguished?, from: :distinguished
356
+
357
+ # @!attribute [r] media
358
+ # @return [Hash] media details
359
+ property :media
360
+
361
+ # @!attribute [r] upvote_ratio
362
+ # @return [Float] the upvote ratio (ups/downs)
363
+ property :upvote_ratio
364
+
365
+ # @!attribute [r] mod_reports
366
+ # @return [Array] moderator reports
367
+ property :mod_reports
368
+
369
+ # @!attribute [r] self?
370
+ # @return [Boolean] whether the post is a self post
371
+ property :self?, from: :is_self
372
+
373
+ # @!attribute [r] visited?
374
+ # @return [Boolean] whether the post was visited
375
+ property :visited?, from: :visited
376
+
377
+ # @!attribute [r] subreddit_type
378
+ # @return [String] the subreddit's type
379
+ property :subreddit_type
380
+
381
+ # @!attribute [r] video?
382
+ # @return [Boolean] whether the post is probably a video
383
+ property :video, from: :is_video
384
+
385
+ private
386
+
387
+ def lazer_reload # rubocop:disable Metrics/AbcSize
388
+ fully_loaded!
389
+
390
+ # Ensure we have the link's id.
391
+ id = exists_locally?(:id) ? read_attribute(:id) : read_attribute(:name).sub('t3_', '')
392
+
393
+ # If a specific sort order was requested, provide it.
394
+ params = {}
395
+ params[:sort] = read_attribute(:sort_order) if exists_locally?(:sort_order)
396
+
397
+ # `response` is a pair (2-element array):
398
+ # - response[0] is a one-item listing containing the submission
399
+ # - response[1] is listing of comments
400
+ response = client.get("/comments/#{id}", params).body
401
+ response[0][:data][:children][0][:data].merge(comments: response[1][:data])
402
+ end
403
+ end
404
+ end
405
+ end