redd 0.7.10 → 0.8.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -30
  3. data/.rspec +1 -1
  4. data/.rubocop.yml +16 -3
  5. data/.travis.yml +13 -7
  6. data/Gemfile +3 -1
  7. data/LICENSE.txt +21 -0
  8. data/README.md +40 -126
  9. data/Rakefile +10 -3
  10. data/TODO.md +11 -0
  11. data/bin/console +84 -0
  12. data/bin/setup +8 -0
  13. data/lib/redd.rb +84 -46
  14. data/lib/redd/api_client.rb +109 -0
  15. data/lib/redd/auth_strategies/auth_strategy.rb +60 -0
  16. data/lib/redd/auth_strategies/installed.rb +22 -0
  17. data/lib/redd/auth_strategies/script.rb +23 -0
  18. data/lib/redd/auth_strategies/userless.rb +17 -0
  19. data/lib/redd/auth_strategies/web.rb +29 -0
  20. data/lib/redd/client.rb +88 -0
  21. data/lib/redd/error.rb +19 -142
  22. data/lib/redd/models/access.rb +20 -0
  23. data/lib/redd/models/basic_model.rb +124 -0
  24. data/lib/redd/models/comment.rb +51 -0
  25. data/lib/redd/models/front_page.rb +71 -0
  26. data/lib/redd/models/inboxable.rb +23 -0
  27. data/lib/redd/models/lazy_model.rb +63 -0
  28. data/lib/redd/models/listing.rb +26 -0
  29. data/lib/redd/models/messageable.rb +20 -0
  30. data/lib/redd/models/moderatable.rb +41 -0
  31. data/lib/redd/models/more_comments.rb +10 -0
  32. data/lib/redd/models/multireddit.rb +32 -0
  33. data/lib/redd/models/postable.rb +70 -0
  34. data/lib/redd/models/private_message.rb +29 -0
  35. data/lib/redd/models/replyable.rb +16 -0
  36. data/lib/redd/models/session.rb +86 -0
  37. data/lib/redd/models/submission.rb +40 -0
  38. data/lib/redd/models/subreddit.rb +201 -0
  39. data/lib/redd/models/user.rb +72 -0
  40. data/lib/redd/models/wiki_page.rb +24 -0
  41. data/lib/redd/utilities/error_handler.rb +35 -0
  42. data/lib/redd/utilities/rate_limiter.rb +21 -0
  43. data/lib/redd/utilities/stream.rb +63 -0
  44. data/lib/redd/utilities/unmarshaller.rb +39 -0
  45. data/lib/redd/version.rb +4 -3
  46. data/logo.png +0 -0
  47. data/redd.gemspec +26 -22
  48. metadata +73 -99
  49. data/LICENSE.md +0 -22
  50. data/RedditKit.LICENSE.md +0 -9
  51. data/lib/redd/access.rb +0 -76
  52. data/lib/redd/clients/base.rb +0 -188
  53. data/lib/redd/clients/base/account.rb +0 -20
  54. data/lib/redd/clients/base/identity.rb +0 -22
  55. data/lib/redd/clients/base/none.rb +0 -27
  56. data/lib/redd/clients/base/privatemessages.rb +0 -33
  57. data/lib/redd/clients/base/read.rb +0 -113
  58. data/lib/redd/clients/base/stream.rb +0 -81
  59. data/lib/redd/clients/base/submit.rb +0 -19
  60. data/lib/redd/clients/base/utilities.rb +0 -104
  61. data/lib/redd/clients/base/wikiread.rb +0 -33
  62. data/lib/redd/clients/installed.rb +0 -57
  63. data/lib/redd/clients/script.rb +0 -41
  64. data/lib/redd/clients/userless.rb +0 -32
  65. data/lib/redd/clients/web.rb +0 -58
  66. data/lib/redd/objects/base.rb +0 -39
  67. data/lib/redd/objects/comment.rb +0 -22
  68. data/lib/redd/objects/labeled_multi.rb +0 -13
  69. data/lib/redd/objects/listing.rb +0 -29
  70. data/lib/redd/objects/more_comments.rb +0 -11
  71. data/lib/redd/objects/private_message.rb +0 -28
  72. data/lib/redd/objects/submission.rb +0 -139
  73. data/lib/redd/objects/subreddit.rb +0 -330
  74. data/lib/redd/objects/thing.rb +0 -26
  75. data/lib/redd/objects/thing/editable.rb +0 -22
  76. data/lib/redd/objects/thing/hideable.rb +0 -18
  77. data/lib/redd/objects/thing/inboxable.rb +0 -25
  78. data/lib/redd/objects/thing/messageable.rb +0 -34
  79. data/lib/redd/objects/thing/moderatable.rb +0 -43
  80. data/lib/redd/objects/thing/refreshable.rb +0 -14
  81. data/lib/redd/objects/thing/saveable.rb +0 -21
  82. data/lib/redd/objects/thing/votable.rb +0 -33
  83. data/lib/redd/objects/user.rb +0 -52
  84. data/lib/redd/objects/wiki_page.rb +0 -15
  85. data/lib/redd/rate_limit.rb +0 -88
  86. data/lib/redd/response/parse_json.rb +0 -18
  87. data/lib/redd/response/raise_error.rb +0 -16
  88. data/spec/redd/objects/base_spec.rb +0 -1
  89. data/spec/redd/response/raise_error_spec.rb +0 -11
  90. data/spec/redd_spec.rb +0 -5
  91. data/spec/spec_helper.rb +0 -71
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'basic_model'
4
+
5
+ module Redd
6
+ module Models
7
+ # Models access_token and related keys.
8
+ class Access < BasicModel
9
+ def expired?(grace_period = 60)
10
+ Time.now > @created_at + (get_attribute(:expires_in) - grace_period)
11
+ end
12
+
13
+ private
14
+
15
+ def after_initialize
16
+ @created_at = Time.now
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Redd
4
+ module Models
5
+ # The base class for all models.
6
+ class BasicModel
7
+ class << self
8
+ # @abstract Create an instance from a partial hash containing an id. The difference between
9
+ # this and {#initialize} is that from_response is supposed to know how to build the whole
10
+ # response from the partial.
11
+ # @param client [APIClient] the api client to initialize the object with
12
+ # @param hash [Hash] a partial hash
13
+ # @return [BasicModel]
14
+ def from_response(client, hash)
15
+ new(client, hash)
16
+ end
17
+
18
+ # @abstract Create an instance from a value.
19
+ # @param _client [APIClient] the api client to initialize the object with
20
+ # @param _value [Object] the object to coerce
21
+ # @return [BasicModel]
22
+ def from_id(_client, _value)
23
+ # TODO: abstract this out?
24
+ raise "coercion not implemented for #{name}"
25
+ end
26
+
27
+ # @return [Hash<Symbol, #from_id>] a mapping of keys to models
28
+ def coerced_attributes
29
+ @coerced_attributes ||= {}
30
+ end
31
+
32
+ # Mark an attribute to coerce.
33
+ # @param name [Symbol] the attribute to coerce
34
+ # @param model [#from_id, nil] a model to coerce it to
35
+ def coerce_attribute(name, model = nil)
36
+ coerced_attributes[name] = model
37
+ end
38
+ end
39
+
40
+ # @return [APIClient] the client the model was initialized with
41
+ attr_reader :client
42
+
43
+ # Create a non-lazily initialized class.
44
+ # @param client [APIClient] the client that the model uses to make requests
45
+ # @param attributes [Hash] the class's attributes
46
+ def initialize(client, attributes = {})
47
+ @client = client
48
+ @attributes = attributes
49
+ @to_coerce = self.class.coerced_attributes.keys
50
+ after_initialize
51
+ end
52
+
53
+ # @return [Hash] a Hash representation of the object
54
+ def to_h
55
+ coerce_all_attributes
56
+ @attributes
57
+ end
58
+
59
+ # Checks whether an attribute is supported by method_missing.
60
+ # @param method_name [Symbol] the method name or attribute to check
61
+ # @param include_private [Boolean] whether to also include private methods
62
+ # @return [Boolean] whether the method is handled by method_missing
63
+ def respond_to_missing?(method_name, include_private = false)
64
+ attribute?(method_name) || attribute?(depredicate(method_name)) || super
65
+ end
66
+
67
+ # Return an attribute or raise a NoMethodError if it doesn't exist.
68
+ # @param method_name [Symbol] the name of the attribute
69
+ # @return [Object] the result of the attribute check
70
+ def method_missing(method_name, *args, &block)
71
+ return get_attribute(method_name) if attribute?(method_name)
72
+ return get_attribute(depredicate(method_name)) if attribute?(depredicate(method_name))
73
+ super
74
+ end
75
+
76
+ private
77
+
78
+ # @abstract Lets us plug in custom code without making a mess
79
+ def after_initialize; end
80
+
81
+ # Coerces an attribute into a class using the {.from_id} method.
82
+ # @param attribute [Symbol] the attribute to coerce
83
+ def coerce_attribute(attribute)
84
+ return unless @to_coerce.include?(attribute) && @attributes.include?(attribute)
85
+ klass = self.class.coerced_attributes.fetch(attribute)
86
+ @attributes[attribute] =
87
+ if klass.nil?
88
+ @client.unmarshal(@attributes[attribute])
89
+ else
90
+ klass.from_id(@client, @attributes[attribute])
91
+ end
92
+ @to_coerce.delete(attribute)
93
+ end
94
+
95
+ # Coerce every attribute that can be coerced.
96
+ def coerce_all_attributes
97
+ @to_coerce.each { |a| coerce_attribute(a) }
98
+ end
99
+
100
+ # Remove a trailing '?' from a symbol name.
101
+ # @param method_name [Symbol] the symbol to "depredicate"
102
+ # @return [Symbol] the symbol but with the '?' removed
103
+ def depredicate(method_name)
104
+ method_name.to_s.chomp('?').to_sym
105
+ end
106
+
107
+ # Get an attribute, raising KeyError if not present.
108
+ # @param name [Symbol] the attribute to check and get
109
+ # @return [Object] the value of the attribute
110
+ def get_attribute(name)
111
+ # Coerce the attribute if it exists and needs to be coerced.
112
+ coerce_attribute(name) if @to_coerce.include?(name) && @attributes.key?(name)
113
+ # Fetch the attribute, raising a KeyError if it doesn't exist.
114
+ @attributes.fetch(name)
115
+ end
116
+
117
+ # @param name [Symbol] the name of the attribute to check
118
+ # @return [Boolean] whether the attribute exists
119
+ def attribute?(name)
120
+ @attributes.key?(name)
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lazy_model'
4
+ require_relative 'inboxable'
5
+ require_relative 'moderatable'
6
+ require_relative 'postable'
7
+ require_relative 'replyable'
8
+
9
+ require_relative 'listing'
10
+ require_relative 'subreddit'
11
+ require_relative 'user'
12
+
13
+ module Redd
14
+ module Models
15
+ # A comment.
16
+ class Comment < LazyModel
17
+ include Inboxable
18
+ include Moderatable
19
+ include Postable
20
+ include Replyable
21
+
22
+ coerce_attribute :replies
23
+ coerce_attribute :author, User
24
+ coerce_attribute :subreddit, Subreddit
25
+
26
+ # Make a Comment from its id.
27
+ # @option hash [String] :name the comment's fullname (e.g. t1_abc123)
28
+ # @option hash [String] :id the comment's id (e.g. abc123)
29
+ # @return [Comment]
30
+ def self.from_response(client, hash)
31
+ # Ensure we have the comment's id.
32
+ id = hash.fetch(:id, hash.fetch(:name).tr('t1_', ''))
33
+
34
+ # If we have the link_id, we can load the listing with replies.
35
+ if hash.key?(:link_id)
36
+ link_id = hash[:link_id].tr('t3_', '')
37
+ return new(client, hash) do |c|
38
+ # The second half contains a single-item listing containing the comment
39
+ c.get("/comments/#{link_id}/_/#{id}").body[1][:data][:children][0][:data]
40
+ end
41
+ end
42
+
43
+ # We can only load the comment in isolation if we don't have the link_id.
44
+ new(client, hash) do |c|
45
+ # Returns a single-item listing containing the comment
46
+ c.get('/api/info', id: "t1_#{id}").body[:data][:children][0][:data]
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'basic_model'
4
+ require_relative '../utilities/stream'
5
+
6
+ module Redd
7
+ module Models
8
+ # The front page.
9
+ # FIXME: deal with serious code duplication from Subreddit
10
+ class FrontPage < BasicModel
11
+ # @return [Array<String>] reddit's base wiki pages
12
+ def wiki_pages
13
+ @client.get('/wiki/pages').body[:data]
14
+ end
15
+
16
+ # Get a wiki page by its title.
17
+ # @param title [String] the page's title
18
+ # @return [WikiPage]
19
+ def wiki_page(title)
20
+ WikiPage.from_response(@client, title: title)
21
+ end
22
+
23
+ # Get the appropriate listing.
24
+ # @param sort [:hot, :new, :top, :controversial, :comments, :rising] the type of listing
25
+ # @param params [Hash] a list of params to send with the request
26
+ # @option params [String] :after return results after the given fullname
27
+ # @option params [String] :before return results before the given fullname
28
+ # @option params [Integer] :count the number of items already seen in the listing
29
+ # @option params [1..100] :limit the maximum number of things to return
30
+ # @option params [:hour, :day, :week, :month, :year, :all] :time the time period to consider
31
+ # when sorting.
32
+ #
33
+ # @note The option :time only applies to the top and controversial sorts.
34
+ # @return [Listing<Submission>]
35
+ def listing(sort, **params)
36
+ params[:t] = params.delete(:time) if params.key?(:time)
37
+ @client.model(:get, "/#{sort}.json", params)
38
+ end
39
+
40
+ # @!method hot(**params)
41
+ # @!method new(**params)
42
+ # @!method top(**params)
43
+ # @!method controversial(**params)
44
+ # @!method comments(**params)
45
+ # @!method rising(**params)
46
+ #
47
+ # @see #listing
48
+ %i(hot new top controversial comments rising).each do |sort|
49
+ define_method(sort) { |**params| listing(sort, **params) }
50
+ end
51
+
52
+ # Stream newly submitted posts.
53
+ def post_stream(**params, &block)
54
+ params[:limit] ||= 100
55
+ stream = Utilities::Stream.new do |before|
56
+ listing(:new, params.merge(before: before))
57
+ end
58
+ block_given? ? stream.stream(&block) : stream.enum_for(:stream)
59
+ end
60
+
61
+ # Stream newly submitted comments.
62
+ def comment_stream(**params, &block)
63
+ params[:limit] ||= 100
64
+ stream = Utilities::Stream.new do |before|
65
+ listing(:comments, params.merge(before: before))
66
+ end
67
+ block_given? ? stream.stream(&block) : stream.enum_for(:stream)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Redd
4
+ module Models
5
+ # Things that can be sent to a user's inbox.
6
+ module Inboxable
7
+ # Block the user that sent this item.
8
+ def block
9
+ @client.post('/api/block', id: get_attribute(:fullname))
10
+ end
11
+
12
+ # Mark this thing as read.
13
+ def mark_as_read
14
+ @client.post('/api/read_message', id: get_attribute(:fullname))
15
+ end
16
+
17
+ # Mark one or more messages as unread.
18
+ def mark_as_unread
19
+ @client.post('/api/unread_message', id: get_attribute(:fullname))
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'basic_model'
4
+
5
+ module Redd
6
+ module Models
7
+ # The base class for lazily-initializable models.
8
+ class LazyModel < BasicModel
9
+ # Create a lazily initialized class.
10
+ # @param client [APIClient] the client that the model uses to make requests
11
+ # @param base_attributes [Hash] the already-known attributes that do not need to be looked up
12
+ # @yield [client] the model's client
13
+ # @yieldparam client [APIClient]
14
+ # @yieldreturn [Hash] the response of "initializing" the lazy model
15
+ def initialize(client, base_attributes = {}, &block)
16
+ super(client, base_attributes)
17
+ @lazy_loader = block
18
+ @definitely_fully_loaded = false
19
+ end
20
+
21
+ # @return [Boolean] whether the model can be to be lazily initialized
22
+ def lazy?
23
+ !@lazy_loader.nil?
24
+ end
25
+
26
+ # Force the object to make a request to reddit.
27
+ # @return [self]
28
+ def force_load
29
+ return unless lazy?
30
+ @attributes.merge!(@lazy_loader.call(@client))
31
+ @definitely_fully_loaded = true
32
+ self
33
+ end
34
+ alias reload force_load
35
+
36
+ # Convert the object to a hash, making a request to fetch additional attributes if needed.
37
+ # @return [Hash]
38
+ def to_h
39
+ ensure_fully_loaded
40
+ super
41
+ end
42
+
43
+ private
44
+
45
+ # Make sure the model is loaded at least once.
46
+ def ensure_fully_loaded
47
+ force_load if lazy? && !@definitely_fully_loaded
48
+ end
49
+
50
+ # Gets the attribute and loads it if it may be available from the response.
51
+ def get_attribute(name)
52
+ ensure_fully_loaded unless @attributes.key?(name)
53
+ super
54
+ end
55
+
56
+ # Checks whether an attribute exists, loading it first if necessary.
57
+ def attribute?(name)
58
+ ensure_fully_loaded unless @attributes.key?(name)
59
+ super
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'basic_model'
4
+
5
+ module Redd
6
+ module Models
7
+ # A backward-expading listing of items.
8
+ # @see Stream
9
+ class Listing < BasicModel
10
+ include Enumerable
11
+
12
+ # Make a Listing from a basic Hash.
13
+ # @return [Listing]
14
+ def self.from_response(client, hash)
15
+ hash[:children].map! { |el| client.unmarshal(el) }
16
+ new(client, hash)
17
+ end
18
+
19
+ %i([] each empty? length size).each do |method_name|
20
+ define_method(method_name) do |*args, &block|
21
+ get_attribute(:children).public_send(method_name, *args, &block)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Redd
4
+ module Models
5
+ # A model that can be messaged (i.e. Users and Subreddits).
6
+ module Messageable
7
+ # Compose a message to a person or the moderators of a subreddit.
8
+ #
9
+ # @param to [String] the thing to send the message to (overriden by User and Subreddit)
10
+ # @param subject [String] the subject of the message
11
+ # @param text [String] the message text
12
+ # @param from [Subreddit, nil] the subreddit to send the message on behalf of
13
+ def send_message(to:, subject:, text:, from: nil)
14
+ params = { to: to, subject: subject, text: text }
15
+ params[:from_sr] = from.display_name if from
16
+ @client.post('/api/compose', params)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Redd
4
+ module Models
5
+ # A model that can be managed by a moderator (i.e. Submissions and Comments).
6
+ module Moderatable
7
+ # Approve a submission.
8
+ def approve
9
+ @client.post('/api/approve', id: get_attribute(:name))
10
+ end
11
+
12
+ # Remove a submission.
13
+ # @param spam [Boolean] whether or not this item is removed due to it being spam
14
+ def remove(spam: false)
15
+ @client.post('/api/remove', id: get_attribute(:name), spam: spam)
16
+ end
17
+
18
+ # Distinguish a link or comment with a sigil to show that it has
19
+ # been created by a moderator.
20
+ # @param how [:yes, :no, :admin, :special] how to distinguish the thing
21
+ def distinguish(how = :yes)
22
+ @client.post('/api/distinguish', id: get_attribute(:name), how: how)
23
+ end
24
+
25
+ # Remove the sigil that shows a thing was created by a moderator.
26
+ def undistinguish
27
+ distinguish(:no)
28
+ end
29
+
30
+ # Stop getting any moderator-related reports on the thing.
31
+ def ignore_reports
32
+ @client.post('/api/ignore_reports', id: get_attribute(:name))
33
+ end
34
+
35
+ # Start getting moderator-related reports on the thing again.
36
+ def unignore_reports
37
+ @client.post('/api/unignore_reports', id: get_attribute(:name))
38
+ end
39
+ end
40
+ end
41
+ end