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,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require_relative 'model'
5
+
6
+ module Redd
7
+ module Models
8
+ # Represents a message in the new modmail.
9
+ class ModmailMessage < Model
10
+ # @!attribute [r] id
11
+ # @return [String] the message id
12
+ property :id
13
+
14
+ # @!attribute [r] body
15
+ # @return [String] the html conversation body
16
+ property :body
17
+
18
+ # @!attribute [r] markdown_body
19
+ # @return [String] the body in markdown form
20
+ property :markdown_body, from: :bodyMarkdown
21
+
22
+ # @!attribute [r] author
23
+ # @return [Object] FIXME: do shit
24
+ property :author
25
+
26
+ # @!attribute [r] internal?
27
+ # @return [Boolean] whether the message is internal
28
+ property :internal?, from: :isInternal
29
+
30
+ # @!attribute [r] date
31
+ # @return [Time] the message date
32
+ property :date, with: ->(t) { Time.parse(t) }
33
+ end
34
+ end
35
+ end
@@ -1,20 +1,20 @@
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
  # An object that represents a bunch of comments that need to be expanded.
8
- class MoreComments < BasicModel
8
+ class MoreComments < Model
9
9
  # Expand the object's children into a listing of Comments and MoreComments.
10
10
  # @param link [Submission] the submission the object belongs to
11
11
  # @param sort [String] the sort order of the submission
12
12
  # @return [Listing<Comment, MoreComments>] the expanded children
13
13
  def expand(link:, sort: nil)
14
- params = { link_id: link.name, children: get_attribute(:children).join(',') }
14
+ params = { link_id: link.name, children: read_attribute(:children).join(',') }
15
15
  params[:sort] = sort if sort
16
16
  params[:sort] = link.sort_order if link.sort_order
17
- @client.model(:get, '/api/morechildren', params)
17
+ client.model(:get, '/api/morechildren', params)
18
18
  end
19
19
 
20
20
  # Keep expanding until all top-level MoreComments are converted to comments.
@@ -44,8 +44,32 @@ module Redd
44
44
 
45
45
  # @return [Array<String>] an array representation of self
46
46
  def to_ary
47
- get_attribute(:children)
47
+ read_attribute(:children)
48
48
  end
49
+
50
+ # @!attribute [r] count
51
+ # @return [Integer] the comments under this object
52
+ property :count
53
+
54
+ # @!attribute [r] name
55
+ # @return [String] the object fullname
56
+ property :name
57
+
58
+ # @!attribute [r] id
59
+ # @return [String] the object id
60
+ property :id
61
+
62
+ # @!attribute [r] parent_id
63
+ # @return [String] the parent fullname
64
+ property :parent_id
65
+
66
+ # @!attribute [r] depth
67
+ # @return [Integer] the depth
68
+ property :depth
69
+
70
+ # @!attribute [r] children
71
+ # @return [Array<String>] the unexpanded comments
72
+ property :children
49
73
  end
50
74
  end
51
75
  end
@@ -1,19 +1,11 @@
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 multi.
8
- class Multireddit < LazyModel
9
- # Create a Multireddit from its path.
10
- # @param client [APIClient] the api client to initialize the object with
11
- # @param id [String] the multi's path (with a leading and trailing slash)
12
- # @return [Multireddit]
13
- def self.from_id(client, id)
14
- new(client, path: id)
15
- end
16
-
8
+ class Multireddit < Model
17
9
  # Get the appropriate listing.
18
10
  # @param sort [:hot, :new, :top, :controversial, :comments, :rising, :gilded] the type of
19
11
  # listing
@@ -29,7 +21,7 @@ module Redd
29
21
  # @return [Listing<Submission>]
30
22
  def listing(sort, **params)
31
23
  params[:t] = params.delete(:time) if params.key?(:time)
32
- @client.model(:get, "#{get_attribute(:path)}#{sort}", params)
24
+ client.model(:get, "#{read_attribute(:path)}#{sort}", params)
33
25
  end
34
26
 
35
27
  # @!method hot(**params)
@@ -41,20 +33,71 @@ module Redd
41
33
  # @!method gilded(**params)
42
34
  #
43
35
  # @see #listing
44
- %i(hot new top controversial comments rising gilded).each do |sort|
36
+ %i[hot new top controversial comments rising gilded].each do |sort|
45
37
  define_method(sort) { |**params| listing(sort, **params) }
46
38
  end
47
39
 
48
- private
40
+ # @!attribute [r] can_edit?
41
+ # @return [Boolean] whether the user can edit the multireddit
42
+ property :can_edit?, from: :can_edit
49
43
 
50
- def after_initialize
51
- @attributes[:subreddits].map! do |subreddit|
52
- Subreddit.new(client, display_name: subreddit[:name])
53
- end
54
- end
44
+ # @!attribute [r] display_name
45
+ # @return [String] the multi's display name
46
+ property :display_name
47
+
48
+ # @!attribute [r] name
49
+ # @return [String] the multireddit name
50
+ property :name
51
+
52
+ # @!attribute [r] description_md
53
+ # @return [String] the markdown verion of the description
54
+ property :description_md
55
+
56
+ # @!attribute [r] description_html
57
+ # @return [String] the html-rendered description
58
+ property :description_html
59
+
60
+ # @!attribute [r] copied_from
61
+ # @return [Multireddit, nil] the multi this one was copied from
62
+ property :copied_from, with: ->(n) { Multireddit.new(client, path: n) if n }
63
+
64
+ # @!attribute [r] icon_url
65
+ # @return [String, nil] the icon url
66
+ property :icon_url
67
+
68
+ # @!attribute [r] subreddits
69
+ # @return [Array<Subreddit>] the subreddits in this multi
70
+ property :subreddits,
71
+ with: ->(a) { a.map { |n| Subreddit.new(client, display_name: n.fetch(:name)) } }
72
+
73
+ # @!attribute [r] created_at
74
+ # @return [Time] the creation time
75
+ property :created_at, from: :created_utc, with: ->(t) { Time.at(t) }
76
+
77
+ # @!attribute [r] key_color
78
+ # @return [String] a hex color
79
+ property :key_color
80
+
81
+ # @!attribute [r] visibility
82
+ # @return [String] the multi visibility, either "public" or "private"
83
+ property :visibility
84
+
85
+ # @!attribute [r] icon_name
86
+ # @return [String] the icon name
87
+ property :icon_name
88
+
89
+ # @!attribute [r] weighting_scheme
90
+ # @return [String]
91
+ property :weighting_scheme
92
+
93
+ # @!attribute [r] path
94
+ # @return [String] the multi path
95
+ property :path, :required
96
+
97
+ private
55
98
 
56
- def default_loader
57
- @client.get("/api/multi#{@attributes.fetch(:path)}").body[:data]
99
+ def lazer_reload
100
+ client.get("/api/multi#{read_attribute(:path)}").body[:data]
58
101
  end
59
102
  end
60
103
  end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model'
4
+
5
+ module Redd
6
+ module Models
7
+ # An enumerable type that covers listings and expands forwards.
8
+ class PaginatedListing
9
+ include Enumerable
10
+
11
+ # A simple fixed-size ring buffer.
12
+ # @api private
13
+ class RingBuffer
14
+ def initialize(size)
15
+ @size = size
16
+ @backing_array = Array.new(size)
17
+ @pointer = 0
18
+ end
19
+
20
+ def include?(el)
21
+ @backing_array.include?(el)
22
+ end
23
+
24
+ def add(el)
25
+ @backing_array[@pointer] = el
26
+ @pointer = (@pointer + 1) % @size
27
+ end
28
+ end
29
+
30
+ # Create an expandable listing.
31
+ # @param client [APIClient] the caller to use for streams
32
+ # @param options [Hash]
33
+ # @option options [String] :before the listing's before parameter
34
+ # @option options [String] :after the listing's after parameter
35
+ # @option options [Integer] :limit the maximum number of items to fetch
36
+ # @yieldparam after [String] the fullname of the item to fetch after
37
+ # @yieldparam limit [Integer] the number of items to fetch (max 100)
38
+ # @yieldreturn [Listing] the listing to return
39
+ def initialize(client, **options, &block)
40
+ raise ArgumentError, 'block must be provided' unless block_given?
41
+
42
+ @client = client
43
+ @caller = block
44
+ @before = options[:before]
45
+ @after = options[:after]
46
+ @limit = options[:limit] || 1000
47
+ end
48
+
49
+ # Go forward through the listing.
50
+ # @yield [Model] the object returned in the listings
51
+ # @return [Enumerator] if a block wasn't provided
52
+ def each(&block)
53
+ return _each(&block) if block_given?
54
+ enum_for(:_each)
55
+ end
56
+
57
+ # Stream through the listing.
58
+ # @note If you iterate through the stream, you'll loop forever.
59
+ # This may or may not be desirable.
60
+ # @yield [Model] the object returned in the listings
61
+ # @return [Enumerator] if a block wasn't provided
62
+ def stream(&block)
63
+ return _stream(&block) if block_given?
64
+ enum_for(:_stream)
65
+ end
66
+
67
+ private
68
+
69
+ # Go backward through the listing.
70
+ # @yield [Object] the object returned in the listings
71
+ def _stream(&block)
72
+ reverse_each(&block) if @limit > 0
73
+ buffer = RingBuffer.new(100)
74
+ loop do
75
+ remaining = fetch_prev_listing
76
+ remaining.reverse_each do |o|
77
+ next if buffer.include?(o)
78
+ yield o
79
+ buffer.add(o)
80
+ end
81
+ end
82
+ end
83
+
84
+ # Go forward through the listing.
85
+ # @yield [Object] the object returned in the listings
86
+ def _each(&block)
87
+ loop do
88
+ return if @limit == 0
89
+ remaining = fetch_next_listing
90
+ return if remaining.children.empty? # if the fetched listing is empty
91
+ remaining.each(&block)
92
+ return if remaining.after.nil? # if there's no link to the next item
93
+ end
94
+ end
95
+
96
+ # Fetch the next listing with @caller and update @after and @limit.
97
+ def fetch_next_listing
98
+ caller_limit = [@limit, 100].min
99
+ listing = @caller.call(before: nil, after: @after, limit: caller_limit)
100
+ @after = listing.after
101
+ @limit -= caller_limit
102
+ listing
103
+ end
104
+
105
+ # Fetch the previous listing with @caller and update @before.
106
+ def fetch_prev_listing
107
+ listing = @caller.call(before: @before, after: nil, limit: 100)
108
+ @before = listing.first.name unless listing.empty?
109
+ listing
110
+ end
111
+ end
112
+ end
113
+ end
@@ -8,37 +8,36 @@ module Redd
8
8
  # @param text [String] The new text.
9
9
  # @return [self] the edited thing
10
10
  def edit(text)
11
- @client.post('/api/editusertext', thing_id: get_attribute(:name), text: text)
12
- @attributes[is_a?(Submission) ? :selftext : :body] = text
11
+ client.post('/api/editusertext', thing_id: read_attribute(:name), text: text)
13
12
  self
14
13
  end
15
14
 
16
15
  # Delete the thing.
17
16
  def delete
18
- @client.post('/api/del', id: get_attribute(:name))
17
+ client.post('/api/del', id: read_attribute(:name))
19
18
  end
20
19
 
21
20
  # Save a link or comment to the user's account.
22
21
  # @param category [String] a category to save to
23
22
  def save(category = nil)
24
- params = { id: get_attribute(:name) }
23
+ params = { id: read_attribute(:name) }
25
24
  params[:category] = category if category
26
- @client.post('/api/save', params)
25
+ client.post('/api/save', params)
27
26
  end
28
27
 
29
28
  # Remove the link or comment from the user's saved links.
30
29
  def unsave
31
- @client.post('/api/unsave', id: get_attribute(:name))
30
+ client.post('/api/unsave', id: read_attribute(:name))
32
31
  end
33
32
 
34
33
  # Hide a link from the user.
35
34
  def hide
36
- @client.post('/api/hide', id: get_attribute(:name))
35
+ client.post('/api/hide', id: read_attribute(:name))
37
36
  end
38
37
 
39
38
  # Unhide a previously hidden link.
40
39
  def unhide
41
- @client.post('/api/unhide', id: get_attribute(:name))
40
+ client.post('/api/unhide', id: read_attribute(:name))
42
41
  end
43
42
 
44
43
  # Upvote the model.
@@ -58,12 +57,12 @@ module Redd
58
57
 
59
58
  # Send replies to this thing to the user's inbox.
60
59
  def enable_inbox_replies
61
- @client.post('/api/sendreplies', id: get_attribute(:name), state: true)
60
+ client.post('/api/sendreplies', id: read_attribute(:name), state: true)
62
61
  end
63
62
 
64
63
  # Stop sending replies to this thing to the user's inbox.
65
64
  def disable_inbox_replies
66
- @client.post('/api/sendreplies', id: get_attribute(:name), state: false)
65
+ client.post('/api/sendreplies', id: read_attribute(:name), state: false)
67
66
  end
68
67
 
69
68
  private
@@ -71,9 +70,8 @@ module Redd
71
70
  # Send a vote.
72
71
  # @param direction [-1, 0, 1] the direction to vote in
73
72
  def vote(direction)
74
- fullname = get_attribute(:name)
75
- @client.post('/api/vote', id: fullname, dir: direction)
76
- @attributes[:ups] += direction
73
+ fullname = read_attribute(:name)
74
+ client.post('/api/vote', id: fullname, dir: direction)
77
75
  end
78
76
  end
79
77
  end
@@ -1,38 +1,105 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'lazy_model'
3
+ require_relative 'model'
4
4
  require_relative 'inboxable'
5
5
  require_relative 'replyable'
6
+ require_relative 'reportable'
6
7
 
7
8
  module Redd
8
9
  module Models
9
10
  # A private message
10
- class PrivateMessage < LazyModel
11
+ class PrivateMessage < Model
11
12
  include Inboxable
12
13
  include Replyable
14
+ include Reportable
13
15
 
14
16
  # Delete the message from the user's inbox.
15
17
  def delete
16
- @client.post('/api/del_msg', id: get_attribute(:name))
18
+ client.post('/api/del_msg', id: read_attribute(:name))
17
19
  end
18
20
 
19
21
  # Mute the author of the message.
20
22
  def mute_author
21
- @client.post('/api/mute_message_author', id: get_attribute(:name))
23
+ client.post('/api/mute_message_author', id: read_attribute(:name))
22
24
  end
23
25
 
24
26
  # Unmute the author of the message.
25
27
  def unmute_author
26
- @client.post('/api/unmute_message_author', id: get_attribute(:name))
28
+ client.post('/api/unmute_message_author', id: read_attribute(:name))
27
29
  end
28
30
 
29
- private
31
+ # @!attribute [r] first_message
32
+ # @return [Integer] not sure what this does
33
+ property :first_message
30
34
 
31
- def default_loader
32
- # FIXME: This returns the entire conversation, not the specific message. Possible to search,
33
- # because depth of replies is just one.
34
- {}
35
- end
35
+ # @!attribute [r] first_message_name
36
+ # @return [String] the fullname of the first message
37
+ property :first_message_name
38
+
39
+ # @!attribute [r] subreddit
40
+ # @return [Subreddit, nil] the subreddit that sent the message
41
+ property :subreddit, with: ->(s) { Subreddit.new(client, display_name: s) if s }
42
+
43
+ # @!attribute [r] replies
44
+ # @return [Listing<PrivateMessage>]
45
+ property :replies, with: ->(l) { Listing.new(client, l[:data]) if l.is_a?(Hash) }
46
+
47
+ # @!attribute [r] id
48
+ # @return [String] the message id
49
+ property :id
50
+
51
+ # @!attribute [r] subject
52
+ # @return [String] the message subject
53
+ property :subject
54
+
55
+ # @!attribute [r] was_comment?
56
+ # @return [Boolean]
57
+ property :was_comment?, from: :was_comment
58
+
59
+ # @!attribute [r] author
60
+ # @return [User] the message author
61
+ property :author, with: ->(n) { User.new(client, name: n) if n }
62
+
63
+ # @!attribute [r] num_comments
64
+ # @return [Integer] huh?
65
+ property :num_comments
66
+
67
+ # @!attribute [r] parent_id
68
+ # @return [String, nil] the parent id
69
+ property :parent_id
70
+
71
+ # @!attribute [r] subreddit_name_prefixed
72
+ # @return [String] the subreddit name, prefixed with "r/"
73
+ property :subreddit_name_prefixed
74
+
75
+ # @!attribute [r] new?
76
+ # @return [Boolean] whether the message is new
77
+ property :new?, from: :new
78
+
79
+ # @!attribute [r] body
80
+ # @return [String] the message body
81
+ property :body
82
+
83
+ # @!attribute [r] body_html
84
+ # @return [String] the html-rendered version of the body
85
+ property :body_html
86
+
87
+ # @!attribute [r] dest
88
+ # @return [String] the recipient of the message
89
+ # @todo maybe convert the object to Subreddit/User?
90
+ property :dest
91
+
92
+ # @!attribute [r] name
93
+ # @return [String] the message fullname
94
+ property :name
95
+
96
+ # @!attribute [r] created
97
+ # @return [Time] the time the message was created
98
+ property :created_utc, with: ->(t) { Time.at(t) }
99
+
100
+ # @!attribute [r] distinguished
101
+ # @return [String] the level the message is distinguished
102
+ property :distinguished
36
103
  end
37
104
  end
38
105
  end