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,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