redd 0.8.8 → 0.9.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
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.