neonredd 0.0.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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE.md +22 -0
  4. data/README.md +1 -0
  5. data/Rakefile +5 -0
  6. data/Redd.LICENSE.md +22 -0
  7. data/RedditKit.LICENSE.md +9 -0
  8. data/lib/redd/access.rb +76 -0
  9. data/lib/redd/clients/base/account.rb +20 -0
  10. data/lib/redd/clients/base/identity.rb +22 -0
  11. data/lib/redd/clients/base/none.rb +27 -0
  12. data/lib/redd/clients/base/privatemessages.rb +33 -0
  13. data/lib/redd/clients/base/read.rb +114 -0
  14. data/lib/redd/clients/base/stream.rb +82 -0
  15. data/lib/redd/clients/base/submit.rb +19 -0
  16. data/lib/redd/clients/base/utilities.rb +143 -0
  17. data/lib/redd/clients/base/wikiread.rb +33 -0
  18. data/lib/redd/clients/base.rb +181 -0
  19. data/lib/redd/clients/installed.rb +56 -0
  20. data/lib/redd/clients/script.rb +40 -0
  21. data/lib/redd/clients/userless.rb +32 -0
  22. data/lib/redd/clients/web.rb +59 -0
  23. data/lib/redd/error.rb +151 -0
  24. data/lib/redd/objects/base.rb +39 -0
  25. data/lib/redd/objects/comment.rb +22 -0
  26. data/lib/redd/objects/labeled_multi.rb +13 -0
  27. data/lib/redd/objects/listing.rb +29 -0
  28. data/lib/redd/objects/more_comments.rb +10 -0
  29. data/lib/redd/objects/private_message.rb +28 -0
  30. data/lib/redd/objects/submission.rb +140 -0
  31. data/lib/redd/objects/subreddit.rb +329 -0
  32. data/lib/redd/objects/thing/editable.rb +22 -0
  33. data/lib/redd/objects/thing/hideable.rb +18 -0
  34. data/lib/redd/objects/thing/inboxable.rb +25 -0
  35. data/lib/redd/objects/thing/messageable.rb +38 -0
  36. data/lib/redd/objects/thing/moderatable.rb +43 -0
  37. data/lib/redd/objects/thing/refreshable.rb +14 -0
  38. data/lib/redd/objects/thing/saveable.rb +21 -0
  39. data/lib/redd/objects/thing/votable.rb +33 -0
  40. data/lib/redd/objects/thing.rb +26 -0
  41. data/lib/redd/objects/user.rb +52 -0
  42. data/lib/redd/objects/wiki_page.rb +15 -0
  43. data/lib/redd/rate_limit.rb +88 -0
  44. data/lib/redd/response/parse_json.rb +18 -0
  45. data/lib/redd/response/raise_error.rb +16 -0
  46. data/lib/redd/version.rb +4 -0
  47. data/lib/redd.rb +50 -0
  48. data/neonredd.gemspec +33 -0
  49. data/spec/redd/objects/base_spec.rb +1 -0
  50. data/spec/redd/response/raise_error_spec.rb +11 -0
  51. data/spec/redd_spec.rb +5 -0
  52. data/spec/spec_helper.rb +71 -0
  53. metadata +225 -0
@@ -0,0 +1,181 @@
1
+ require 'faraday'
2
+ require_relative '../version'
3
+ require_relative '../response/raise_error'
4
+ require_relative '../response/parse_json'
5
+ require_relative '../rate_limit'
6
+ require_relative '../access'
7
+
8
+ module Redd
9
+ # The module containing the multiple types of clients.
10
+ module Clients
11
+ # The basic client to inherit from. Don't use this directly, prefer
12
+ # {Redd.it}
13
+ class Base
14
+ # @!parse include Utilities
15
+ # @!parse include Account
16
+ # @!parse include Identity
17
+ # @!parse include None
18
+ # @!parse include Privatemessages
19
+ # @!parse include Read
20
+ # @!parse include Submit
21
+ # @!parse include Stream
22
+ # @!parse include Wikiread
23
+ %w(
24
+ utilities account identity none privatemessages read submit stream
25
+ wikiread
26
+ ).each do |mixin_name|
27
+ require_relative "base/#{mixin_name}"
28
+ camel_case = mixin_name.split('_').map(&:capitalize).join
29
+ include const_get(camel_case)
30
+ end
31
+
32
+ # @!attribute [r] user_agent
33
+ # @return [String] The user-agent used to communicate with reddit.
34
+ attr_reader :user_agent
35
+
36
+ # @!attribute [r] rate_limit
37
+ # @return [#after_limit] The handler that takes care of rate limiting.
38
+ attr_reader :rate_limit
39
+
40
+ # @!attribute [r] auth_endpoint
41
+ # @return [String] The site to connect to for authentication.
42
+ attr_reader :auth_endpoint
43
+
44
+ # @!attribute [r] api_endpoint
45
+ # @return [String] The site to make API requests with.
46
+ attr_reader :api_endpoint
47
+
48
+ # @!attribute [rw] access
49
+ # @return [Access] The access object to make API requests with.
50
+ attr_accessor :access
51
+
52
+ # Create a Client.
53
+ #
54
+ # @param [Hash] options The options to create the client with.
55
+ # @option options [String] :user_agent The User-Agent string to use in
56
+ # the header of every request.
57
+ # @option options [#after_limit] :rate_limit The handler that takes care
58
+ # of rate limiting.
59
+ # @option options [String] :auth_endpoint The main domain to authenticate
60
+ # with.
61
+ # @option options [String] :api_endpoint The main domain to make requests
62
+ # with.
63
+ # @note HTTPS is mandatory for OAuth2.
64
+ def initialize(**options)
65
+ @user_agent = options[:user_agent] || "Redd/Ruby, v#{Redd::VERSION}"
66
+ @rate_limit = options[:rate_limit] || RateLimit.new(1)
67
+ @auth_endpoint = options[:auth_endpoint] || 'https://www.reddit.com/'
68
+ @api_endpoint = options[:api_endpoint] || 'https://oauth.reddit.com/'
69
+ @access = Access.new(expires_at: Time.at(0))
70
+ end
71
+
72
+ # @!method get(path, params = {})
73
+ # @!method post(path, params = {})
74
+ # @!method put(path, params = {})
75
+ # @!method patch(path, params = {})
76
+ # @!method delete(path, params = {})
77
+ #
78
+ # Sends the request to the given path with the given params and return
79
+ # the body of the response.
80
+ # @param [String] path The path under the api_endpoint to request.
81
+ # @param [Hash] params The parameters to send with the request.
82
+ # @return [Faraday::Response] The response.
83
+ [:get, :post, :put, :patch, :delete].each do |meth|
84
+ define_method(meth) do |path, params = {}|
85
+ @rate_limit.after_limit do
86
+ final_params = default_params.merge(params)
87
+ connection.send(meth, path, final_params)
88
+ end
89
+ end
90
+ end
91
+
92
+ # @param [Access] new_access The access to use.
93
+ # @yield The client with the given access.
94
+ def with(new_access)
95
+ old_access = @access
96
+ @access = new_access
97
+ response = yield(self)
98
+ @access = old_access
99
+ response
100
+ end
101
+
102
+ # Obtain a new access token using a refresh token.
103
+ # @return [Access] The refreshed information.
104
+ def refresh_access!
105
+ response = auth_connection.post(
106
+ '/api/v1/access_token',
107
+ grant_type: 'refresh_token',
108
+ refresh_token: access.refresh_token
109
+ )
110
+ access.refreshed!(response.body)
111
+ end
112
+
113
+ # Dispose of an access or refresh token when you're done with it.
114
+ # @param [Boolean] remove_refresh_token Whether or not to remove all
115
+ # tokens associated with the user.
116
+ def revoke_access!(remove_refresh_token = false)
117
+ token_type = remove_refresh_token ? :refresh_token : :access_token
118
+ token = access.send(token_type)
119
+ @access = nil
120
+ auth_connection.post(
121
+ '/api/v1/revoke_token',
122
+ token: token,
123
+ token_type_hint: token_type
124
+ )
125
+ end
126
+
127
+ private
128
+
129
+ # @return [Faraday::RackBuilder] The middleware to use when creating the
130
+ # connection.
131
+ def middleware
132
+ @middleware ||= Faraday::RackBuilder.new do |builder|
133
+ builder.use Response::RaiseError
134
+ builder.use Response::ParseJson
135
+ builder.use Faraday::Request::Multipart
136
+ builder.use Faraday::Request::UrlEncoded
137
+ builder.adapter Faraday.default_adapter
138
+ end
139
+ end
140
+
141
+ # @return [Hash] The minimum parameters to send with every request.
142
+ def default_params
143
+ @default_params ||= { api_type: 'json' }
144
+ end
145
+
146
+ # @return [Hash] A hash of the headers used.
147
+ def default_headers
148
+ {
149
+ 'User-Agent' => @user_agent,
150
+ 'Authorization' => "bearer #{@access.access_token}"
151
+ }
152
+ end
153
+
154
+ # @return [Faraday::Connection] A new or existing connection.
155
+ def connection
156
+ @connection ||= Faraday.new(
157
+ @api_endpoint,
158
+ headers: default_headers,
159
+ builder: middleware
160
+ )
161
+ end
162
+
163
+ # @return [Hash] A hash of the headers with basic auth.
164
+ def auth_headers
165
+ {
166
+ 'User-Agent' => @user_agent,
167
+ 'Authorization' => Faraday.basic_auth(@client_id, @secret)
168
+ }
169
+ end
170
+
171
+ # @return [Faraday::Connection] A new or existing connection.
172
+ def auth_connection
173
+ @auth_connection ||= Faraday.new(
174
+ @auth_endpoint,
175
+ headers: auth_headers,
176
+ builder: middleware
177
+ )
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,56 @@
1
+ require 'cgi'
2
+ require_relative 'base'
3
+
4
+ module Redd
5
+ module Clients
6
+ # The client for installed apps that can't keep a secret.
7
+ # It might even work with Rubymotion (fingers crossed).
8
+ class Installed < Base
9
+ # @!attribute [r] client_id
10
+ attr_reader :client_id
11
+
12
+ # @!attribute [r] redirect_uri
13
+ attr_reader :redirect_uri
14
+
15
+ # @param [Hash] options The options to create the client with.
16
+ # @see Base#initialize
17
+ # @see Redd.it
18
+ def initialize(client_id, redirect_uri, **options)
19
+ @client_id = client_id
20
+ @redirect_uri = redirect_uri
21
+ super(**options)
22
+ end
23
+
24
+ # @param [String] state A random string to double-check later.
25
+ # @param [Array<String>] scope The scope to request access to.
26
+ # @param [:temporary, :permanent] duration
27
+ # @return [String] The url to redirect the user to.
28
+ # rubocop:disable Metrics/MethodLength
29
+ def auth_url(state, scope = ['identity'], duration = :temporary)
30
+ query = {
31
+ response_type: 'token',
32
+ client_id: @client_id,
33
+ redirect_uri: @redirect_uri,
34
+ state: state,
35
+ scope: scope.join(','),
36
+ duration: duration
37
+ }
38
+ url = URI.join(auth_endpoint, '/api/v1/authorize')
39
+ url.query = URI.encode_www_form(query)
40
+ url.to_s
41
+ end
42
+
43
+ # Authorize using the url fragment.
44
+ # @param [String] fragment The part of the url after the "#".
45
+ # @return [Access] The access given by reddit.
46
+ def authorize!(fragment)
47
+ parsed = CGI.parse(fragment)
48
+ @access = Access.new(
49
+ access_token: parsed[:access_token].first,
50
+ expires_in: parsed[:expires_in].first,
51
+ scope: parsed[:scope]
52
+ )
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,40 @@
1
+ require_relative 'base'
2
+
3
+ module Redd
4
+ module Clients
5
+ # The client for an account you own (e.g. bots).
6
+ class Script < Base
7
+ # @!attribute [r] client_id
8
+ attr_reader :client_id
9
+
10
+ # @!attribute [r] username
11
+ attr_reader :username
12
+
13
+ # @param [Hash] options The options to create the client with.
14
+ # @see Base#initialize
15
+ # @see Redd.it
16
+ def initialize(client_id, secret, username, password, **options)
17
+ @client_id = client_id
18
+ @secret = secret
19
+ @username = username
20
+ @password = password
21
+ super(**options)
22
+ end
23
+
24
+ # Authorize using the given data.
25
+ # @return [Access] The access given by reddit.
26
+ def authorize!
27
+ response = auth_connection.post(
28
+ '/api/v1/access_token',
29
+ grant_type: 'password',
30
+ username: @username,
31
+ password: @password
32
+ )
33
+
34
+ @access = Access.new(response.body)
35
+ end
36
+
37
+ alias refresh_access! authorize!
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,32 @@
1
+ require_relative 'base'
2
+
3
+ module Redd
4
+ module Clients
5
+ # The client that doesn't need a user to function.
6
+ # @note Of course, that means many editing methods throw an error.
7
+ class Userless < Base
8
+ # @!attribute [r] client_id
9
+ attr_reader :client_id
10
+
11
+ # @param [Hash] options The options to create the client with.
12
+ # @see Base#initialize
13
+ # @see Redd.it
14
+ def initialize(client_id, secret, **options)
15
+ @client_id = client_id
16
+ @secret = secret
17
+ super(**options)
18
+ end
19
+
20
+ # Authorize using the given data.
21
+ # @return [Access] The access given by reddit.
22
+ def authorize!
23
+ response = auth_connection.post(
24
+ '/api/v1/access_token',
25
+ grant_type: 'client_credentials'
26
+ )
27
+
28
+ @access = Access.new(response.body)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,59 @@
1
+ require 'uri'
2
+ require_relative 'base'
3
+
4
+ module Redd
5
+ module Clients
6
+ # The client for a web-based flow (e.g. "login with reddit")
7
+ class Web < Base
8
+ # @!attribute [r] client_id
9
+ attr_reader :client_id
10
+
11
+ # @!attribute [r] redirect_uri
12
+ attr_reader :redirect_uri
13
+
14
+ # @param [Hash] options The options to create the client with.
15
+ # @see Base#initialize
16
+ # @see Redd.it
17
+ def initialize(client_id, secret, redirect_uri, **options)
18
+ @client_id = client_id
19
+ @secret = secret
20
+ @redirect_uri = redirect_uri
21
+ super(**options)
22
+ end
23
+
24
+ # @param [String] state A random string to double-check later.
25
+ # @param [Array<String>] scope The scope to request access to.
26
+ # @param [:temporary, :permanent] duration
27
+ # @return [String] The url to redirect the user to.
28
+ # rubocop:disable Metrics/MethodLength
29
+ def auth_url(state, scope = ['identity'], duration = :temporary)
30
+ query = {
31
+ response_type: 'code',
32
+ client_id: @client_id,
33
+ redirect_uri: @redirect_uri,
34
+ state: state,
35
+ scope: scope.join(','),
36
+ duration: duration
37
+ }
38
+
39
+ url = URI.join(auth_endpoint, '/api/v1/authorize')
40
+ url.query = URI.encode_www_form(query)
41
+ url.to_s
42
+ end
43
+
44
+ # Authorize using the code given.
45
+ # @param [String] code The code from the get params.
46
+ # @return [Access] The access given by reddit.
47
+ def authorize!(code)
48
+ response = auth_connection.post(
49
+ '/api/v1/access_token',
50
+ grant_type: 'authorization_code',
51
+ code: code,
52
+ redirect_uri: @redirect_uri
53
+ )
54
+
55
+ @access = Access.new(response.body)
56
+ end
57
+ end
58
+ end
59
+ end
data/lib/redd/error.rb ADDED
@@ -0,0 +1,151 @@
1
+ module Redd
2
+ # An error from reddit
3
+ # TODO: Move Error to an Errors module in next minor version?
4
+ class Error < StandardError
5
+ attr_reader :code
6
+ attr_reader :headers
7
+ attr_reader :body
8
+
9
+ def initialize(env)
10
+ @code = env[:status]
11
+ @headers = env[:response_headers]
12
+ @body = env[:body]
13
+ end
14
+
15
+ def self.from_response(env) # rubocop:disable all
16
+ status = env[:status]
17
+ body = parse_error(env[:body]).to_s
18
+ case status
19
+ when 200
20
+ case body
21
+ when /access_denied/i then OAuth2AccessDenied
22
+ when /unsupported_response_type/i then InvalidResponseType
23
+ when /unsupported_grant_type/i then InvalidGrantType
24
+ when /invalid_scope/i then InvalidScope
25
+ when /invalid_request/i then InvalidRequest
26
+ when /no_text/i then NoTokenGiven
27
+ when /invalid_grant/i then ExpiredCode
28
+ when /wrong_password/i then InvalidCredentials
29
+ when /bad_captcha/i then InvalidCaptcha
30
+ when /ratelimit/i then RateLimited
31
+ when /quota_filled/i then QuotaFilled
32
+ when /bad_css_name/i then InvalidClassName
33
+ when /too_old/i then Archived
34
+ when /too_much_flair_css/i then TooManyClassNames
35
+ when /user_required/i then AuthenticationRequired
36
+ end
37
+ when 400 then BadRequest
38
+ when 401 then InvalidOAuth2Credentials
39
+ when 403
40
+ if /user_required/i =~ body
41
+ AuthenticationRequired
42
+ else
43
+ PermissionDenied
44
+ end
45
+ when 404 then NotFound
46
+ when 409 then Conflict
47
+ when 500 then InternalServerError
48
+ when 502 then BadGateway
49
+ when 503 then ServiceUnavailable
50
+ when 504 then TimedOut
51
+ end
52
+ end
53
+
54
+ def self.parse_error(body) # rubocop:disable all
55
+ return body unless body.is_a?(Hash)
56
+
57
+ if body.key?(:json) && body[:json].key?(:errors)
58
+ body[:json][:errors].first
59
+ elsif body.key?(:jquery)
60
+ body[:jquery]
61
+ elsif body.key?(:error)
62
+ body[:error]
63
+ elsif body.key?(:code) && body[:code] == 'NO_TEXT'
64
+ 'NO_TEXT'
65
+ end
66
+ end
67
+
68
+ # This item has been archived and can no longer be edited.
69
+ Archived = Class.new(Error)
70
+
71
+ AuthenticationRequired = Class.new(Error)
72
+
73
+ # Bad Gateway. Either a network or a reddit error. Either way, try again.
74
+ BadGateway = Class.new(Error)
75
+
76
+ BadRequest = Class.new(Error)
77
+
78
+ Conflict = Class.new(Error)
79
+
80
+ # You already received an access token using this code. The user should
81
+ # grant you access again to get a new code.
82
+ ExpiredCode = Class.new(Error)
83
+
84
+ # There is an issue on reddit's end. Try again.
85
+ InternalServerError = Class.new(Error)
86
+
87
+ InvalidCaptcha = Class.new(Error)
88
+
89
+ InvalidClassName = Class.new(Error)
90
+
91
+ # Either your username or your password is wrong.
92
+ InvalidCredentials = Class.new(Error)
93
+
94
+ InvalidGrantType = Class.new(Error)
95
+
96
+ InvalidMultiredditName = Class.new(Error)
97
+
98
+ # Your client id or your secret is wrong.
99
+ InvalidOAuth2Credentials = Class.new(Error)
100
+
101
+ InvalidResponseType = Class.new(Error)
102
+
103
+ InvalidRequest = Class.new(Error)
104
+
105
+ # You don't have the proper scope to perform this request.
106
+ InvalidScope = Class.new(Error)
107
+
108
+ # Looks like we didn't get a JSON response. Raise this error.
109
+ JSONError = Class.new(Error)
110
+
111
+ # Four, oh four! The thing you're looking for wasn't found.
112
+ NotFound = Class.new(Error)
113
+
114
+ # No access token was given.
115
+ NoTokenGiven = Class.new(Error)
116
+
117
+ OAuth2AccessDenied = Class.new(Error)
118
+
119
+ PermissionDenied = Class.new(Error)
120
+
121
+ # Raised when the client needs to wait before making another request
122
+ class RateLimited < Error
123
+ # @!attribute [r] time
124
+ # @return [Integer] the seconds to wait before making another request.
125
+ attr_reader :time
126
+
127
+ def initialize(env)
128
+ super
129
+ @time = env[:body][:json][:ratelimit] || 3600
130
+ end
131
+ end
132
+
133
+ # This seems to be a more OAuth2-focused error.
134
+ class QuotaFilled < RateLimited
135
+ def initialize(env)
136
+ super
137
+ @time = 3600
138
+ end
139
+ end
140
+
141
+ RequestError = Class.new(Error)
142
+
143
+ # Issue on reddit's end. Try again.
144
+ ServiceUnavailable = Class.new(Error)
145
+
146
+ # The connection timed out. Try again.
147
+ TimedOut = Class.new(Error)
148
+
149
+ TooManyClassNames = Class.new(Error)
150
+ end
151
+ end
@@ -0,0 +1,39 @@
1
+ require 'hashie'
2
+ require 'forwardable'
3
+
4
+ module Redd
5
+ # A bunch of objects that can hold properties.
6
+ module Objects
7
+ # A base for all objects to inherit from.
8
+ class Base < Hashie::Hash
9
+ include Hashie::Extensions::MergeInitializer
10
+ include Hashie::Extensions::MethodReader
11
+ include Hashie::Extensions::MethodQuery
12
+ include Hashie::Extensions::DeepMerge
13
+
14
+ # The `delete` method is called `delete_path` because it conflicts with
15
+ # Hash#delete.
16
+ extend Forwardable
17
+ def_delegators :@client, :get, :post, :put, :delete_path
18
+
19
+ # @!attribute [r] client
20
+ # @return [Clients::Base] The client that used to make requests.
21
+ attr_reader :client
22
+
23
+ # @param [Clients::Base] client The client instance.
24
+ # @param [Hash] attributes A hash of attributes.
25
+ def initialize(client, attributes = {})
26
+ @client = client
27
+ super(attributes)
28
+ end
29
+
30
+ # Define an alias for a property.
31
+ # @param [Symbol] new_name The alias.
32
+ # @param [Symbol] old_name The existing property.
33
+ def self.alias_property(new_name, old_name)
34
+ define_method(new_name) { send(old_name) }
35
+ define_method(:"#{new_name}?") { send(old_name) }
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,22 @@
1
+ require_relative 'thing'
2
+
3
+ module Redd
4
+ module Objects
5
+ # A comment that can be made on a link.
6
+ class Comment < Thing
7
+ include Thing::Editable
8
+ include Thing::Inboxable
9
+ include Thing::Moderatable
10
+ include Thing::Refreshable
11
+ include Thing::Saveable
12
+ include Thing::Votable
13
+
14
+ alias_property :reports_count, :num_reports
15
+
16
+ # @return [Listing] The comment's replies.
17
+ def replies
18
+ @replies ||= (client.object_from_body(self[:replies]) || [])
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ module Redd
2
+ module Objects
3
+ # A comment that can be made on a link.
4
+ class LabeledMulti < Base
5
+ # @see Objects::Base
6
+ def initialize(client, attributes = {})
7
+ attr_dup = attributes.dup
8
+ attr_dup[:subreddits].map! { |sub| sub[:name] }
9
+ super(client, attr_dup)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ module Redd
2
+ module Objects
3
+ # A collection of reddit things.
4
+ # @see https://www.reddit.com/dev/api#listings
5
+ class Listing < Array
6
+ # @!attribute [r] before
7
+ # @return [String] The id of the object before the listing.
8
+ attr_reader :before
9
+
10
+ # @!attribute [r] after
11
+ # @return [String] The id of the object after the listing.
12
+ attr_reader :after
13
+
14
+ # @param [Clients::Base] client The client to expand the comments with.
15
+ # @param [{:before => String, :after => String,
16
+ # :children => Array<Hash>}] attributes The data to initialize the
17
+ # class with.
18
+ # @todo Only call Clients::Base#object_from_body when item is being
19
+ # accessed.
20
+ def initialize(client, attributes)
21
+ @before = attributes[:before]
22
+ @after = attributes[:after]
23
+ attributes[:children].each do |child|
24
+ self << (client.object_from_body(child) || child)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,10 @@
1
+ module Redd
2
+ module Objects
3
+ # The model for a morecomments object
4
+ class MoreComments < Array
5
+ def initialize(_, attributes)
6
+ super(attributes[:children])
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,28 @@
1
+ require_relative 'thing'
2
+
3
+ module Redd
4
+ module Objects
5
+ # The model for private messages
6
+ class PrivateMessage < Thing
7
+ include Thing::Inboxable
8
+
9
+ alias_property :from, :author
10
+ alias_property :to, :dest
11
+
12
+ # Block the sender of the message from sending any more.
13
+ def block_sender!
14
+ post('/api/block', id: fullname)
15
+ end
16
+
17
+ # Mark the message as read.
18
+ def mark_as_read
19
+ post('/api/read_message', id: fullname)
20
+ end
21
+
22
+ # Mark the message as unread and add orangered to account.
23
+ def mark_as_unread
24
+ post('/api/unread_message', id: fullname)
25
+ end
26
+ end
27
+ end
28
+ end