redd 0.8.8 → 0.9.0.pre.1

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -1
  3. data/CONTRIBUTING.md +63 -0
  4. data/Guardfile +7 -0
  5. data/README.md +6 -5
  6. data/Rakefile +1 -1
  7. data/TODO.md +423 -0
  8. data/bin/console +91 -77
  9. data/bin/guard +2 -0
  10. data/lib/redd.rb +7 -5
  11. data/lib/redd/api_client.rb +2 -3
  12. data/lib/redd/auth_strategies/auth_strategy.rb +7 -2
  13. data/lib/redd/auth_strategies/script.rb +7 -0
  14. data/lib/redd/auth_strategies/userless.rb +7 -0
  15. data/lib/redd/auth_strategies/web.rb +6 -1
  16. data/lib/redd/client.rb +0 -3
  17. data/lib/redd/errors.rb +56 -0
  18. data/lib/redd/middleware.rb +10 -8
  19. data/lib/redd/models/access.rb +30 -18
  20. data/lib/redd/models/comment.rb +185 -27
  21. data/lib/redd/models/front_page.rb +16 -36
  22. data/lib/redd/models/gildable.rb +1 -1
  23. data/lib/redd/models/inboxable.rb +13 -3
  24. data/lib/redd/models/listing.rb +27 -6
  25. data/lib/redd/models/live_thread.rb +76 -23
  26. data/lib/redd/models/live_update.rb +46 -0
  27. data/lib/redd/models/messageable.rb +1 -1
  28. data/lib/redd/models/mod_action.rb +59 -0
  29. data/lib/redd/models/model.rb +23 -0
  30. data/lib/redd/models/moderatable.rb +6 -6
  31. data/lib/redd/models/modmail.rb +61 -0
  32. data/lib/redd/models/modmail_conversation.rb +154 -0
  33. data/lib/redd/models/modmail_message.rb +35 -0
  34. data/lib/redd/models/more_comments.rb +29 -5
  35. data/lib/redd/models/multireddit.rb +63 -20
  36. data/lib/redd/models/paginated_listing.rb +113 -0
  37. data/lib/redd/models/postable.rb +11 -13
  38. data/lib/redd/models/private_message.rb +78 -11
  39. data/lib/redd/models/replyable.rb +2 -2
  40. data/lib/redd/models/reportable.rb +14 -0
  41. data/lib/redd/models/searchable.rb +2 -2
  42. data/lib/redd/models/self.rb +17 -0
  43. data/lib/redd/models/session.rb +75 -31
  44. data/lib/redd/models/submission.rb +309 -56
  45. data/lib/redd/models/subreddit.rb +330 -103
  46. data/lib/redd/models/trophy.rb +34 -0
  47. data/lib/redd/models/user.rb +185 -46
  48. data/lib/redd/models/wiki_page.rb +37 -16
  49. data/lib/redd/utilities/error_handler.rb +13 -13
  50. data/lib/redd/utilities/unmarshaller.rb +7 -5
  51. data/lib/redd/version.rb +1 -1
  52. data/redd.gemspec +18 -15
  53. metadata +82 -16
  54. data/lib/redd/error.rb +0 -53
  55. data/lib/redd/models/basic_model.rb +0 -80
  56. data/lib/redd/models/lazy_model.rb +0 -75
  57. data/lib/redd/models/mod_mail.rb +0 -142
  58. data/lib/redd/utilities/stream.rb +0 -61
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model'
4
+
5
+ module Redd
6
+ module Models
7
+ # A user trophy.
8
+ class Trophy < Model
9
+ # @!attribute [r] icon_70px
10
+ # @return [String] the url for a 70x70 thumbnail icon
11
+ property :icon_70px, from: :icon_70
12
+
13
+ # @!attribute [r] icon_40px
14
+ # @return [String] the url for a 40x40 thumbnail icon
15
+ property :icon_40px, from: :icon_40
16
+
17
+ # @!attribute [r] name
18
+ # @return [String] the name of the trophy
19
+ property :name
20
+
21
+ # @!attribute [r] id
22
+ # @return [String] the trophy id
23
+ property :id
24
+
25
+ # @!attribute [r] award_id
26
+ # @return [String]
27
+ property :award_id
28
+
29
+ # @!attribute [r] description
30
+ # @return [String] the trophy description
31
+ property :description
32
+ end
33
+ end
34
+ end
@@ -1,53 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'lazy_model'
3
+ require_relative 'model'
4
4
  require_relative 'messageable'
5
5
 
6
6
  module Redd
7
7
  module Models
8
8
  # A reddit user.
9
- class User < LazyModel
9
+ class User < Model
10
10
  include Messageable
11
11
 
12
- # Create a User from their name.
13
- # @param client [APIClient] the api client to initialize the object with
14
- # @param id [String] the username
15
- # @return [User]
16
- def self.from_id(client, id)
17
- new(client, name: id)
18
- end
19
-
20
- # Unblock a previously blocked user.
21
- # @param me [User] (optional) the person doing the unblocking
22
- def unblock(me: nil)
23
- my_id = 't2_' + (me.is_a?(User) ? user.id : @client.get('/api/v1/me').body[:id])
24
- # Talk about an unintuitive endpoint
25
- @client.post('/api/unfriend', container: my_id, name: get_attribute(:name), type: 'enemy')
26
- end
27
-
28
- # Compose a message to the moderators of a subreddit.
29
- #
30
- # @param subject [String] the subject of the message
31
- # @param text [String] the message text
32
- # @param from [Subreddit, nil] the subreddit to send the message on behalf of
33
- def send_message(subject:, text:, from: nil)
34
- super(to: get_attribute(:name), subject: subject, text: text, from: from)
35
- end
36
-
37
- # Add the user as a friend.
38
- # @param note [String] a note for the friend
39
- def friend(note = nil)
40
- name = get_attribute(:name)
41
- body = JSON.generate(note ? { name: name, note: note } : { name: name })
42
- @client.request(:put, "/api/v1/me/friends/#{name}", body: body)
43
- end
44
-
45
- # Unfriend the user.
46
- def unfriend
47
- name = get_attribute(:name)
48
- @client.request(:delete, "/api/v1/me/friends/#{name}", raw: true, form: { id: name })
49
- end
50
-
51
12
  # Get the appropriate listing.
52
13
  # @param type [:overview, :submitted, :comments, :liked, :disliked, :hidden, :saved, :gilded]
53
14
  # the type of listing to request
@@ -65,7 +26,7 @@ module Redd
65
26
  # @return [Listing<Submission>]
66
27
  def listing(type, **params)
67
28
  params[:t] = params.delete(:time) if params.key?(:time)
68
- @client.model(:get, "/user/#{get_attribute(:name)}/#{type}.json", params)
29
+ client.model(:get, "/user/#{read_attribute(:name)}/#{type}.json", params)
69
30
  end
70
31
 
71
32
  # @!method overview(**params)
@@ -78,20 +39,198 @@ module Redd
78
39
  # @!method gilded(**params)
79
40
  #
80
41
  # @see #listing
81
- %i(overview submitted comments liked disliked hidden saved gilded).each do |type|
42
+ %i[overview submitted comments liked disliked hidden saved gilded].each do |type|
82
43
  define_method(type) { |**params| listing(type, **params) }
83
44
  end
84
45
 
46
+ # Compose a message to the moderators of a subreddit.
47
+ #
48
+ # @param subject [String] the subject of the message
49
+ # @param text [String] the message text
50
+ # @param from [Subreddit, nil] the subreddit to send the message on behalf of
51
+ def send_message(subject:, text:, from: nil)
52
+ super(to: read_attribute(:name), subject: subject, text: text, from: from)
53
+ end
54
+
55
+ # Block this user.
56
+ def block
57
+ client.post('/api/block_user', account_id: read_attribute(:id))
58
+ end
59
+
60
+ # @return [Array<Trophy>] this user's trophies
61
+ def trophies
62
+ client.get("/api/v1/user/#{read_attribute(:name)}/trophies")
63
+ .body[:data][:trophies]
64
+ .map { |t| client.unmarshal(t) }
65
+ end
66
+
67
+ # Unblock a previously blocked user.
68
+ # @param me [User] (optional) the person doing the unblocking
69
+ def unblock(me: nil)
70
+ my_id = 't2_' + (me.is_a?(User) ? user.id : client.get('/api/v1/me').body[:id])
71
+ # Talk about an unintuitive endpoint
72
+ client.post('/api/unfriend', container: my_id, name: read_attribute(:name), type: 'enemy')
73
+ end
74
+
75
+ # Add the user as a friend.
76
+ # @param note [String] a note for the friend
77
+ def friend(note = nil)
78
+ name = read_attribute(:name)
79
+ body = JSON.generate(note ? { name: name, note: note } : { name: name })
80
+ client.request(:put, "/api/v1/me/friends/#{name}", body: body)
81
+ end
82
+
83
+ # Unfriend the user.
84
+ def unfriend
85
+ name = read_attribute(:name)
86
+ client.request(:delete, "/api/v1/me/friends/#{name}", raw: true, form: { id: name })
87
+ end
88
+
85
89
  # Gift a redditor reddit gold.
86
90
  # @param months [Integer] the number of months of gold to gift
87
91
  def gift_gold(months: 1)
88
- @client.post("/api/v1/gold/give/#{get_attribute(:name)}", months: months)
92
+ client.post("/api/v1/gold/give/#{read_attribute(:name)}", months: months)
89
93
  end
90
94
 
95
+ # @!attribute [r] name
96
+ # @return [String] the user's username
97
+ property :name
98
+
99
+ # @!attribute [r] employee?
100
+ # @return [Boolean] whether the user is a reddit employee
101
+ property :employee?, from: :is_employee
102
+
103
+ # @!attribute [r] features
104
+ # @return [Hash] a hash of features
105
+ property :features
106
+
107
+ # @!attribute [r] friend?
108
+ # @return [Boolean] whether the user is your friend
109
+ property :friend?, from: :is_friend
110
+
111
+ # @!attribute [r] no_profanity?
112
+ # @return [Boolean] whether the user chooses to filter profanity
113
+ property :no_profanity?, from: :pref_no_profanity
114
+
115
+ # @!attribute [r] suspended?
116
+ # @return [Boolean] whether the user is suspended
117
+ property :suspended?, from: :is_suspended
118
+
119
+ # @!attribute [r] geopopular
120
+ # @return [String]
121
+ property :geopopular, from: :pref_geopopular
122
+
123
+ # @!attribute [r] subreddit
124
+ # @return [Subreddit] the user's personal "subreddit"
125
+ property :subreddit, with: ->(name) { Subreddit.new(client, display_name: name) if name }
126
+
127
+ # @!attribute [r] sponsor?
128
+ # @return [Boolean]
129
+ property :sponsor?, from: :is_sponsor
130
+
131
+ # @!attribute [r] gold_expiration
132
+ # @return [Time, nil] the time when the user's gold expires
133
+ property :gold_expiration, with: ->(epoch) { Time.at(epoch) if epoch }
134
+
135
+ # @!attribute [r] id
136
+ # @return [String] the user's base36 id
137
+ property :id
138
+
139
+ # @!attribute [r] profile_image
140
+ # @return [String] a link to the user's profile image
141
+ property :profile_image, from: :profile_img
142
+
143
+ # @!attribute [r] over_18?
144
+ # @return [Boolean] whether the user's profile is considered over 18.
145
+ property :over_18?, from: :profile_over_18
146
+
147
+ # @!attribute [r] suspension_expiration
148
+ # @return [Time, nil] the time when the user's suspension expires
149
+ property :suspension_expiration, from: :suspension_expiration_utc,
150
+ with: ->(epoch) { Time.at(epoch) if epoch }
151
+
152
+ # @!attribute [r] verified?
153
+ # @return [Boolean] whether the user is verified (?)
154
+ property :verified?, from: :verified
155
+
156
+ # @!attribute [r] new_modmail_exists?
157
+ # @return [Boolean] whether the user has mail in the new modmail
158
+ property :new_modmail_exists?, from: :new_modmail_exists
159
+
160
+ # @!attribute [r] over_18?
161
+ # @return [Boolean] whether the user has indicated they're over 18
162
+ property :over_18?, from: :over_18
163
+
164
+ # @!attribute [r] gold?
165
+ # @return [Boolean] whether the user currently has gold
166
+ property :gold?, from: :is_gold
167
+
168
+ # @!attribute [r] mod?
169
+ # @return [Boolean] whether the user is a moderator
170
+ property :mod?, from: :is_mod
171
+
172
+ # @!attribute [r] has_verified_email?
173
+ # @return [Boolean] whether the user's email has been verified
174
+ property :has_verified_email?, from: :has_verified_email
175
+
176
+ # @!attribute [r] has_mod_mail?
177
+ # @return [Boolean] whether the user has old-style mod mail
178
+ property :has_mod_mail?, from: :has_mod_mail
179
+
180
+ # @!attribute [r] hidden_from_robots?
181
+ # @return [Boolean] whether the user chose to hide from Google
182
+ property :hidden_from_robots?, from: :hide_from_robots
183
+
184
+ # @!attribute [r] link_karma
185
+ # @return [Integer] the user's link karma
186
+ property :link_karma
187
+
188
+ # @!attribute [r] inbox_count
189
+ # @return [Integer] the number of messages in the user's inbox
190
+ property :inbox_count
191
+
192
+ # @!attribute [r] show_top_karma_subreddits?
193
+ # @return [Boolean] whether top karma subreddits are shown on the user's page
194
+ property :show_top_karma_subreddits?, from: :pref_top_karma_subreddits
195
+
196
+ # @!attribute [r] has_mail?
197
+ # @return [Boolean] whether the user has new messages
198
+ property :has_mail?, from: :has_mail
199
+
200
+ # @!attribute [r] show_snoovatar?
201
+ # @return [Boolean] whether the user's snoovatar is shown
202
+ property :show_snoovatar?, from: :pref_show_snoovatar
203
+
204
+ # @!attribute [r] created_at
205
+ # @return [Time] the time the user signed up
206
+ property :created_at, from: :created_utc, with: ->(epoch) { Time.at(epoch) }
207
+
208
+ # @!attribute [r] gold_creddits
209
+ # @return [Integer] the number of gold creddits the user has
210
+ property :gold_creddits
211
+
212
+ # @!attribute [r] in_beta?
213
+ # @return [Boolean] whether the user is in beta
214
+ property :in_beta?, from: :in_beta
215
+
216
+ # @!attribute [r] comment_karma
217
+ # @return [Integer] the user's comment karma
218
+ property :comment_karma
219
+
220
+ # @!attribute [r] has_subscribed?
221
+ # @return [Boolean]
222
+ property :has_subscribed?, from: :has_subscribed
223
+
91
224
  private
92
225
 
93
- def default_loader
94
- @client.get("/user/#{@attributes.fetch(:name)}/about").body[:data]
226
+ def lazer_reload
227
+ # return load_from_fullname if self[:id] && !self[:name]
228
+ fully_loaded!
229
+ client.get("/user/#{read_attribute(:name)}/about").body[:data]
230
+ end
231
+
232
+ def load_from_fullname
233
+ client.get('/api/user_data_by_account_ids', ids: read_attribute(:id)).body.values.first
95
234
  end
96
235
  end
97
236
  end
@@ -1,34 +1,55 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'lazy_model'
3
+ require_relative 'model'
4
4
 
5
5
  module Redd
6
6
  module Models
7
7
  # A reddit user.
8
- class WikiPage < LazyModel
8
+ class WikiPage < Model
9
9
  # Edit the wiki page.
10
10
  # @param content [String] the new wiki page contents
11
11
  # @param reason [String, nil] an optional reason for editing the page
12
12
  def edit(content, reason: nil)
13
- params = { page: @attributes.fetch(:title), content: content }
13
+ params = { page: read_attribute(:title), content: content }
14
14
  params[:reason] = reason if reason
15
- @client.post("/r/#{@attributes.fetch(:subreddit).display_name}/api/wiki/edit", params)
15
+ client.post("/r/#{read_attribute(:subreddit).display_name}/api/wiki/edit", params)
16
16
  end
17
17
 
18
- private
18
+ # @!attribute [r] title
19
+ # @return [String] the page title
20
+ property :title, :required
19
21
 
20
- def default_loader
21
- title = @attributes.fetch(:title)
22
- if @attributes.key?(:subreddit)
23
- sr_name = @attributes[:subreddit].display_name
24
- return @client.get("/r/#{sr_name}/wiki/#{title}").body[:data]
25
- end
26
- @client.get("/wiki/#{title}").body[:data]
27
- end
22
+ # @!attribute [r] subreddit
23
+ # @return [Subreddit] the wiki page's (optional) subreddit
24
+ property :subreddit, :nil
25
+
26
+ # @!attribute [r] may_revise?
27
+ # @return [Boolean] not sure, whether you're allowed to edit the page?
28
+ property :may_revise?, from: :may_revise
29
+
30
+ # @!attribute [r] revision_date
31
+ # @return [Time] the time of the last revision
32
+ property :revision_date, with: ->(t) { Time.at(t) }
33
+
34
+ # @!attribute [r] content_md
35
+ # @return [String] the markdown version of the content
36
+ property :content_md
37
+
38
+ # @!attribute [r] content_html
39
+ # @return [String] the html version of the content
40
+ property :content_html
41
+
42
+ # @!attribute [r] revision_by
43
+ # @return [User] the user who made the last revision
44
+ property :revision_by, with: ->(res) { User.new(client, res[:data]) }
45
+
46
+ private
28
47
 
29
- def after_initialize
30
- return unless @attributes[:revision_by]
31
- @attributes[:revision_by] = @client.unmarshal(@attributes[:revision_by])
48
+ def lazer_reload
49
+ fully_loaded!
50
+ path = "/wiki/#{read_attribute(:title)}"
51
+ path = "/r/#{read_attribute(:subreddit).display_name}#{path}" if self[:subreddit]
52
+ client.get(path).body[:data]
32
53
  end
33
54
  end
34
55
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../error'
3
+ require_relative '../errors'
4
4
 
5
5
  module Redd
6
6
  module Utilities
@@ -11,15 +11,15 @@ module Redd
11
11
  INSUFFICIENT_SCOPE = 'insufficient_scope'
12
12
 
13
13
  HTTP_ERRORS = {
14
- 400 => Redd::BadRequest,
15
- 403 => Redd::Forbidden,
16
- 404 => Redd::NotFound,
17
- 429 => Redd::TooManyRequests,
18
- 500 => Redd::ServerError,
19
- 502 => Redd::ServerError,
20
- 503 => Redd::ServerError,
21
- 504 => Redd::ServerError
22
- }.freeze
14
+ 400 => Errors::BadRequest,
15
+ 403 => Errors::Forbidden,
16
+ 404 => Errors::NotFound,
17
+ 429 => Errors::TooManyRequests,
18
+ 500 => Errors::ServerError,
19
+ 502 => Errors::ServerError,
20
+ 503 => Errors::ServerError,
21
+ 504 => Errors::ServerError
22
+ }
23
23
 
24
24
  def check_error(res, raw:)
25
25
  # Check for status code-based errors first and return it if we found one.
@@ -38,14 +38,14 @@ module Redd
38
38
  def invalid_access_error(res)
39
39
  return nil unless res.code == 401 && res.headers[AUTH_HEADER] &&
40
40
  res.headers[AUTH_HEADER].include?(INVALID_TOKEN)
41
- InvalidAccess.new(res)
41
+ Errors::InvalidAccess.new(res)
42
42
  end
43
43
 
44
44
  # Deal with an error caused by not having enough the correct scope
45
45
  def insufficient_scope_error(res)
46
46
  return nil unless res.code == 403 && res.headers[AUTH_HEADER] &&
47
47
  res.headers[AUTH_HEADER].include?(INSUFFICIENT_SCOPE)
48
- InsufficientScope.new(res)
48
+ Errors::InsufficientScope.new(res)
49
49
  end
50
50
 
51
51
  # Deal with an error signalled by the HTTP response code.
@@ -57,7 +57,7 @@ module Redd
57
57
  def api_error(res)
58
58
  return nil unless res.body.is_a?(Hash) && res.body[:json] && res.body[:json][:errors] &&
59
59
  !res.body[:json][:errors].empty?
60
- APIError.new(res)
60
+ Errors::APIError.new(res)
61
61
  end
62
62
  end
63
63
  end
@@ -7,17 +7,19 @@ module Redd
7
7
  # Contains the mapping from 'kind' strings to classes.
8
8
  # TODO: UserList type!
9
9
  MAPPING = {
10
+ 'Listing' => Models::Listing,
10
11
  't1' => Models::Comment,
11
12
  't2' => Models::User,
12
13
  't3' => Models::Submission,
13
14
  't4' => Models::PrivateMessage,
14
15
  't5' => Models::Subreddit,
16
+ 't6' => Models::Trophy,
15
17
  'more' => Models::MoreComments,
16
18
  'wikipage' => Models::WikiPage,
17
- 'modaction' => Models::Subreddit::ModAction,
19
+ 'modaction' => Models::ModAction,
18
20
  'LabeledMulti' => Models::Multireddit,
19
- 'LiveUpdate' => Models::LiveThread::LiveUpdate
20
- }.freeze
21
+ 'LiveUpdate' => Models::LiveUpdate
22
+ }
21
23
 
22
24
  def initialize(client)
23
25
  @client = client
@@ -25,7 +27,7 @@ module Redd
25
27
 
26
28
  def unmarshal(res)
27
29
  # I'm loving the hell out of this pattern.
28
- model = js_listing(res) || js_model(res) || api_listing(res) || api_model(res)
30
+ model = js_listing(res) || js_model(res) || api_model(res)
29
31
  raise "cannot unmarshal: #{res.inspect}" if model.nil?
30
32
  model
31
33
  end
@@ -43,7 +45,7 @@ module Redd
43
45
  def js_model(res)
44
46
  # FIXME: deprecate this? this shouldn't be happening in the API, so this is better handled
45
47
  # in the respective classes.
46
- Models::BasicModel.new(@client, res[:json][:data]) if res[:json] && res[:json][:data]
48
+ Models::Model.new(@client, res[:json][:data]) if res[:json] && res[:json][:data]
47
49
  end
48
50
 
49
51
  # Unmarshal API-provided listings.