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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'basic_model'
3
+ require_relative 'model'
4
4
 
5
5
  module Redd
6
6
  module Models
@@ -8,35 +8,47 @@ module Redd
8
8
  # @note This model also supports an additional key, called `:created_at` which is a UNIX time
9
9
  # representing the time the access was created. The default value is the time the object was
10
10
  # initialized.
11
- class Access < BasicModel
11
+ class Access < Model
12
12
  # Create a non-lazily initialized Access.
13
- # @param client [Object] (deprecated) the client to create the object with
14
- # @param attributes [Hash] the Access's attributes
13
+ # @param attributes [Hash] the access's attributes
15
14
  # @example
16
15
  # access = Redd::Models::Access.new(access_token: ...)
17
- def initialize(client = nil, attributes = {})
18
- if client.is_a?(Hash)
19
- super(nil, client)
20
- else
21
- super(client, attributes)
22
- end
16
+ def initialize(attributes = {})
17
+ super(nil, attributes)
18
+ @creation_time = Time.now
23
19
  end
24
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
25
24
  def expired?(grace_period = 60)
26
- # We're not sure, so we just assume it hasn't expired.
27
- return false unless @attributes[:expires_in]
28
- Time.now.to_i > @attributes[:created_at] + (@attributes[:expires_in] - grace_period)
25
+ Time.now > read_attribute(:created_at) + read_attribute(:expires_in) - grace_period
29
26
  end
30
27
 
28
+ # @return [Boolean] whether the access can be refreshed
31
29
  def permanent?
32
- !@attributes[:refresh_token].nil?
30
+ read_attribute(:refresh_token).nil?
33
31
  end
34
32
 
35
- private
33
+ # @!attribute [r] access_token
34
+ # @return [String] the access token
35
+ property :access_token
36
36
 
37
- def after_initialize
38
- @attributes[:created_at] ||= Time.now.to_i
39
- end
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(' ') }
40
52
  end
41
53
  end
42
54
  end
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'lazy_model'
3
+ require_relative 'model'
4
4
  require_relative 'gildable'
5
5
  require_relative 'inboxable'
6
6
  require_relative 'moderatable'
7
7
  require_relative 'postable'
8
8
  require_relative 'replyable'
9
+ require_relative 'reportable'
9
10
 
10
11
  require_relative 'listing'
11
12
  require_relative 'subreddit'
@@ -14,47 +15,204 @@ require_relative 'user'
14
15
  module Redd
15
16
  module Models
16
17
  # A comment.
17
- class Comment < LazyModel
18
+ class Comment < Model
18
19
  include Gildable
19
20
  include Inboxable
20
21
  include Moderatable
21
22
  include Postable
22
23
  include Replyable
24
+ include Reportable
23
25
 
24
- # Create a Comment from its fullname.
25
- # @param client [APIClient] the api client to initialize the object with
26
- # @param id [String] the fullname
27
- # @return [Comment]
28
- def self.from_id(client, id)
29
- new(client, name: id)
30
- end
26
+ # @!attribute [r] subreddit_id
27
+ # @return [String] the subreddit fullname
28
+ property :subreddit_id
31
29
 
32
- private
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
33
 
34
- def after_initialize
35
- @attributes[:replies] =
36
- if @attributes[:replies].is_a?(Hash)
37
- @client.unmarshal(@attributes[:replies])
38
- else
39
- Models::Listing.new(@client, children: [], after: nil, before: nil)
40
- end
41
- @attributes[:author] = User.from_id(@client, @attributes.fetch(:author))
42
- @attributes[:subreddit] = Subreddit.from_id(@client, @attributes.fetch(:subreddit))
43
- end
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, with: ->(l) { Listing.new(client, l[:data]) if l.is_a?(Hash) }
53
+
54
+ # @!attribute [r] user_reports
55
+ # @return [Array<String>] user reports
56
+ property :user_reports
57
+
58
+ # @!attribute [r] saved?
59
+ # @return [Boolean] whether the submission was saved by the logged-in user
60
+ property :saved?, from: :saved
61
+
62
+ # @!attribute [r] id
63
+ # @return [String] the comment id
64
+ property :id
65
+
66
+ # @!attribute [r] banned_at
67
+ # @return [Time, nil] the time when the comment was banned
68
+ property :banned_at, from: :banned_at_utc, with: ->(t) { Time.at(t) if t }
69
+
70
+ # @!attribute [r] gilded
71
+ # @return [Integer] the number of times the comment was gilded
72
+ property :gilded
73
+
74
+ # @!attribute [r] archived?
75
+ # @return [Boolean] whether this comment was archived
76
+ property :archived?, from: :archived
77
+
78
+ # @!attribute [r] report_reasons
79
+ # @return [Array<String>] report reasons
80
+ property :report_reasons
81
+
82
+ # @!attribute [r] author
83
+ # @return [User] the comment author
84
+ property :author, with: ->(name) { User.new(client, name: name) }
85
+
86
+ # @!attribute [r] can_mod_post?
87
+ # @return [Boolean] whether the logged-in user can mod post
88
+ property :can_mod_post?, from: :can_mod_post
89
+
90
+ # @!attribute [r] ups
91
+ # @return [Integer] the comment upvotes
92
+ # @deprecated use {#score} instead
93
+ property :ups
94
+
95
+ # @!attribute [r] downs
96
+ # @return [Integer] the comment downvotes
97
+ # @deprecated is always 0; use {#score} instead
98
+ property :downs
99
+
100
+ # @!attribute [r] parent
101
+ # @return [Comment, Submission] the comment parent
102
+ property :parent,
103
+ from: :parent_id,
104
+ with: ->(id) { Session.new(client).from_fullnames(id).first }
105
+
106
+ # @!attribute [r] score
107
+ # @return [Integer] the comment score
108
+ property :score
109
+
110
+ # @!attribute [r] approved_by
111
+ # @return [String] the user that approved the comment
112
+ property :approved_by
113
+
114
+ # @!attribute [r] body
115
+ # @return [String] the markdown comment body
116
+ property :body
117
+
118
+ # @!attribute [r] body_html
119
+ # @return [String] the html-rendered version of the body
120
+ property :body_html
121
+
122
+ # @!attribute [r] edited_at
123
+ # @return [Time, nil] the time when the comment was edited
124
+ property :edited_at, from: :edited, with: ->(t) { Time.at(t) if t }
125
+
126
+ # @!attribute [r] author_flair_css_class
127
+ # @return [String] the author flair css class
128
+ property :author_flair_css_class
129
+
130
+ # @!attribute [r] collapsed?
131
+ # @return [Boolean] whether the comment was collapsed
132
+ property :collapsed?, from: :collapsed
133
+
134
+ # @!attribute [r] submitter?
135
+ # @return [Boolean] whether the comment author is the link submitter
136
+ property :submitter?, from: :is_submitter
137
+
138
+ # @!attribute [r] collapsed_reason
139
+ # @return [String] the reason for collapse (?)
140
+ property :collapsed_reason
141
+
142
+ # @!attribute [r] stickied?
143
+ # @return [Boolean] whether the comment was stickied
144
+ property :stickied?, from: :stickied
145
+
146
+ # @!attribute [r] can_gild?
147
+ # @return [Boolean] whether the comment is gildable
148
+ property :can_gild?, from: :can_gild
149
+
150
+ # @!attribute [r] subreddit
151
+ # @return [Subreddit] the comment's subreddit
152
+ property :subreddit, with: ->(n) { Subreddit.new(client, display_name: n) }
153
+
154
+ # @!attribute [r] score_hidden
155
+ # @return [Boolean] whether the comment score is hidden
156
+ property :score_hidden?, from: :score_hidden
157
+
158
+ # @!attribute [r] subreddit_type
159
+ # @return [String] subreddit type
160
+ property :subreddit_type
161
+
162
+ # @!attribute [r] name
163
+ # @return [String] the comment fullname
164
+ property :name
165
+
166
+ # @!attribute [r] author_flair_text
167
+ # @return [String] the author flair text
168
+ property :author_flair_text
169
+
170
+ # @!attribute [r] created_at
171
+ # @return [String] the time when the model was created
172
+ property :created_at, from: :created_utc, with: ->(t) { Time.at(t) }
173
+
174
+ # @!attribute [r] subreddit_name_prefixed
175
+ # @return [String] the subreddit name, prefixed with "r/"
176
+ property :subreddit_name_prefixed
177
+
178
+ # @!attribute [r] controversiality
179
+ # @return [Integer] the comment controversiality
180
+ property :controversiality
181
+
182
+ # @!attribute [r] depth
183
+ # @return [Integer] the comment depth
184
+ property :depth
185
+
186
+ # @!attribute [r] mod_reports
187
+ # @return [Array<String>] the moderator reports
188
+ property :mod_reports
189
+
190
+ # @!attribute [r] report_count
191
+ # @return [Integer] the report count
192
+ property :report_count, from: :num_reports
193
+
194
+ # @!attribute [r] distinguished?
195
+ # @return [Boolean] whether the comment is distinguished
196
+ property :distinguished?, from: :distinguished
197
+
198
+ private
44
199
 
45
- def default_loader
46
- @attributes.key?(:link_id) ? load_with_comments : load_without_comments
200
+ def lazer_reload
201
+ self[:link] ? load_with_comments : load_without_comments
47
202
  end
48
203
 
49
204
  def load_with_comments
50
- id = @attributes.fetch(:id) { @attributes.fetch(:name).sub('t1_', '') }
51
- link_id = @attributes[:link_id].sub('t3_', '')
52
- @client.get("/comments/#{link_id}/_/#{id}").body[1][:data][:children][0][:data]
205
+ fully_loaded!
206
+ id = self[:id] || read_attribute(:name).sub('t1_', '')
207
+ link_id = read_attribute(:link).name.sub('t3_', '')
208
+ client.get("/comments/#{link_id}/_/#{id}").body[1][:data][:children][0][:data]
53
209
  end
54
210
 
55
211
  def load_without_comments
56
- id = @attributes.fetch(:id) { @attributes.fetch(:name).sub('t1_', '') }
57
- @client.get('/api/info', id: "t1_#{id}").body[:data][:children][0][:data]
212
+ id = self[:id] || read_attribute(:name).sub('t1_', '')
213
+ response = client.get('/api/info', id: "t1_#{id}").body[:data][:children][0][:data]
214
+ response.delete(:replies) # Make sure replies are lazy-loaded later.
215
+ response
58
216
  end
59
217
  end
60
218
  end
@@ -1,41 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'basic_model'
4
- require_relative '../utilities/stream'
3
+ require_relative 'model'
5
4
 
6
5
  module Redd
7
6
  module Models
8
7
  # The front page.
9
8
  # FIXME: deal with serious code duplication from Subreddit
10
- class FrontPage < BasicModel
9
+ class FrontPage < Model
11
10
  # @return [Array<String>] reddit's base wiki pages
12
11
  def wiki_pages
13
- @client.get('/wiki/pages').body[:data]
12
+ client.get('/wiki/pages').body[:data]
14
13
  end
15
14
 
16
15
  # Get a wiki page by its title.
17
16
  # @param title [String] the page's title
18
17
  # @return [WikiPage]
19
18
  def wiki_page(title)
20
- WikiPage.new(@client, title: title)
19
+ WikiPage.new(client, title: title)
21
20
  end
22
21
 
23
22
  # Get the appropriate listing.
24
23
  # @param sort [:hot, :new, :top, :controversial, :comments, :rising, :gilded] the type of
25
24
  # listing
26
- # @param params [Hash] a list of params to send with the request
27
- # @option params [String] :after return results after the given fullname
28
- # @option params [String] :before return results before the given fullname
29
- # @option params [Integer] :count the number of items already seen in the listing
30
- # @option params [1..100] :limit the maximum number of things to return
31
- # @option params [:hour, :day, :week, :month, :year, :all] :time the time period to consider
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
32
30
  # when sorting.
33
31
  #
34
32
  # @note The option :time only applies to the top and controversial sorts.
35
- # @return [Listing<Submission>]
36
- def listing(sort, **params)
37
- params[:t] = params.delete(:time) if params.key?(:time)
38
- @client.model(:get, "/#{sort}", params)
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
39
  end
40
40
 
41
41
  # @!method hot(**params)
@@ -47,29 +47,9 @@ module Redd
47
47
  # @!method gilded(**params)
48
48
  #
49
49
  # @see #listing
50
- %i(hot new top controversial comments rising gilded).each do |sort|
50
+ %i[hot new top controversial comments rising gilded].each do |sort|
51
51
  define_method(sort) { |**params| listing(sort, **params) }
52
52
  end
53
-
54
- # Stream newly submitted posts.
55
- def post_stream(**params, &block)
56
- params[:limit] ||= 100
57
- stream = Utilities::Stream.new do |previous|
58
- before = previous ? previous.first.name : nil
59
- listing(:new, params.merge(before: before))
60
- end
61
- block_given? ? stream.stream(&block) : stream.enum_for(:stream)
62
- end
63
-
64
- # Stream newly submitted comments.
65
- def comment_stream(**params, &block)
66
- params[:limit] ||= 100
67
- stream = Utilities::Stream.new do |previous|
68
- before = previous ? previous.first.name : nil
69
- listing(:comments, params.merge(before: before))
70
- end
71
- block_given? ? stream.stream(&block) : stream.enum_for(:stream)
72
- end
73
53
  end
74
54
  end
75
55
  end
@@ -6,7 +6,7 @@ module Redd
6
6
  module Gildable
7
7
  # Gift a user one month of reddit gold for their link or comment.
8
8
  def gild
9
- @client.post("/api/v1/gold/gild/#{get_attribute(:name)}")
9
+ client.post("/api/v1/gold/gild/#{read_attribute(:name)}")
10
10
  end
11
11
  end
12
12
  end
@@ -6,17 +6,27 @@ module Redd
6
6
  module Inboxable
7
7
  # Block the user that sent this item.
8
8
  def block
9
- @client.post('/api/block', id: get_attribute(:name))
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))
10
20
  end
11
21
 
12
22
  # Mark this thing as read.
13
23
  def mark_as_read
14
- @client.post('/api/read_message', id: get_attribute(:name))
24
+ client.post('/api/read_message', id: read_attribute(:name))
15
25
  end
16
26
 
17
27
  # Mark one or more messages as unread.
18
28
  def mark_as_unread
19
- @client.post('/api/unread_message', id: get_attribute(:name))
29
+ client.post('/api/unread_message', id: read_attribute(:name))
20
30
  end
21
31
  end
22
32
  end
@@ -1,24 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'basic_model'
3
+ require_relative 'model'
4
4
 
5
5
  module Redd
6
6
  module Models
7
7
  # A backward-expading listing of items.
8
8
  # @see Stream
9
- class Listing < BasicModel
9
+ class Listing < Model
10
10
  include Enumerable
11
11
 
12
+ # Create a fully initialized listing.
13
+ # @param client [APIClient] the api client
14
+ # @param attributes [Hash] the attribute hash
15
+ def initialize(client, attributes = {})
16
+ super
17
+ fully_loaded!
18
+ end
19
+
12
20
  # @return [Array<Comment, Submission, PrivateMessage>] an array representation of self
13
- def to_ary
14
- get_attribute(:children)
21
+ def to_a
22
+ read_attribute(:children)
15
23
  end
24
+ alias to_ary to_a
16
25
 
17
- %i([] each empty? first last).each do |method_name|
26
+ %i[[] each empty? first last].each do |method_name|
18
27
  define_method(method_name) do |*args, &block|
19
- get_attribute(:children).public_send(method_name, *args, &block)
28
+ read_attribute(:children).public_send(method_name, *args, &block)
20
29
  end
21
30
  end
31
+
32
+ # @!attribute [r] before
33
+ # @return [String] the fullname of the item before this listing
34
+ property :before, :nil
35
+
36
+ # @!attribute [r] after
37
+ # @return [String] the fullname of the item that the next listing will start from
38
+ property :after, :nil
39
+
40
+ # @!attribute [r] children
41
+ # @return [Array<Model>] the listing's children
42
+ property :children, :required, with: ->(a) { a.map { |m| client.unmarshal(m) } }
22
43
  end
23
44
  end
24
45
  end